版本校验 → 实体化 → 皮肤合成 → 构建 → build-info。任一校验不过 即中止且不触发构建。runBuild 为注入点, 除真构建外全流程可单测。 Cocos 可执行路径只从 COCOS_CREATOR 环境变量取, 缺失显式报错。 同时补入 check-skin.mjs 的 process.argv[1] 守卫,与仓库其余脚本一致。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
3.9 KiB
JavaScript
98 lines
3.9 KiB
JavaScript
#!/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 <game> [-- --platform android]');
|
||
process.exit(1);
|
||
}
|
||
const pIdx = process.argv.indexOf('--platform');
|
||
const targetPlatform = pIdx > -1 ? process.argv[pIdx + 1] : undefined;
|
||
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);
|
||
}
|
||
}
|