#!/usr/bin/env node import { join } from 'node:path'; import { existsSync, rmSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { pathToFileURL } from 'node:url'; import { HOST_PROJECT, GAMES_DIR, FRAMEWORK_SRC, BUILD_WORKSPACE, readCreatorVersion } from './lib/paths.mjs'; import { materialize, composeSkin, SkinValidationError } from './lib/materialize.mjs'; /** 生产用构建实现:调 Cocos CLI。路径只从环境变量取,缺失即显式报错(第二准则)。 */ function runCocosBuild(projectDir, platform) { const exe = process.env.COCOS_CREATOR; if (!exe) { throw new Error( '未设置环境变量 COCOS_CREATOR(Cocos Creator 可执行文件路径)。' + '这是构建的唯一来源,请在环境中配置,不要在脚本里硬编码。', ); } execFileSync(exe, ['--project', projectDir, '--build', `platform=${platform}`], { stdio: 'inherit' }); } /** * 构建一款子游戏:版本校验 → 实体化 → 皮肤合成 → 构建 → 写 build-info。 * opts 可注入路径与 runBuild 用于测试。 */ export function buildGame(name, opts = {}) { const gamesDir = opts.gamesDir ?? GAMES_DIR; const frameworkSrc = opts.frameworkSrc ?? FRAMEWORK_SRC; const hostProject = opts.hostProject ?? HOST_PROJECT; const workspace = opts.workspace ?? BUILD_WORKSPACE; const targetPlatform = opts.platform ?? 'android'; const runBuild = opts.runBuild ?? runCocosBuild; const gameDir = join(gamesDir, name); if (!existsSync(gameDir)) throw new Error(`找不到子游戏工程:${gameDir}`); // 1) 引擎版本一致性(junction 共享资源,版本分叉会损坏真源) const expected = readCreatorVersion(hostProject); const actual = readCreatorVersion(gameDir); if (actual !== expected) { throw new Error(`引擎版本不一致:${name} 为 ${actual},宿主为 ${expected}`); } // 2) 实体化到干净的工作区 const workspaceDir = join(workspace, name); rmSync(workspaceDir, { recursive: true, force: true }); materialize(gameDir, frameworkSrc, workspaceDir); // 3) 皮肤合成(校验不过会抛 SkinValidationError,构建不会被调用) const skin = composeSkin(workspaceDir); // 4) 构建 runBuild(workspaceDir, targetPlatform); // 5) 产物追溯 const buildInfo = { game: name, frameworkCommit: opts.frameworkCommit ?? readFrameworkCommit(), builtAt: opts.now ?? new Date().toISOString(), platform: targetPlatform, skin: { matched: skin.matched.length, frameworkTotal: skin.frameworkTotal }, }; writeFileSync(join(workspaceDir, 'build-info.json'), `${JSON.stringify(buildInfo, null, 2)}\n`); return { workspaceDir, skin: buildInfo.skin, buildInfo }; } function readFrameworkCommit() { try { return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim(); } catch { return null; // 非 git 环境:如实记 null,不编造 } } // CLI 入口(argv[1] 守卫与仓库其余脚本一致:无脚本名时不进 CLI 分支) if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const name = process.argv[2]; if (!name) { console.error('用法: npm run build-game [-- --platform android]'); process.exit(1); } const pIdx = process.argv.indexOf('--platform'); let targetPlatform; if (pIdx > -1) { const val = process.argv[pIdx + 1]; // 用户明确写了 --platform 却没给值(或紧跟下一个 flag):显式报错, // 不能落到 buildGame 的 `?? 'android'` 默认值——那会把"用户指定"悄悄变成"猜的"(第二准则)。 if (!val || val.startsWith('--')) { console.error('用法: npm run build-game [-- --platform ](--platform 后必须跟平台名,如 android)'); process.exit(1); } targetPlatform = val; } try { const r = buildGame(name, targetPlatform ? { platform: targetPlatform } : {}); console.log(`皮肤命中 ${r.skin.matched} 处,框架可覆盖资源共 ${r.skin.frameworkTotal} 个`); console.log(`构建完成:${r.workspaceDir}`); } catch (e) { if (e instanceof SkinValidationError) { console.error(`\n${e.message}(子游戏 ${name}):`); for (const err of e.errors) console.error(` [${err.code}] ${err.path}\n ${err.message}`); } else { console.error(e.message); } process.exit(1); } }