Files
youle_cocos/cocoscreator_projects/scripts/build-game.mjs
T
joywayerandClaude Opus 5 134f4c877c feat(tools): build-game 构建流水线
版本校验 → 实体化 → 皮肤合成 → 构建 → build-info。任一校验不过
即中止且不触发构建。runBuild 为注入点, 除真构建外全流程可单测。
Cocos 可执行路径只从 COCOS_CREATOR 环境变量取, 缺失显式报错。

同时补入 check-skin.mjs 的 process.argv[1] 守卫,与仓库其余脚本一致。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 22:08:48 +08:00

98 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
}
}