f6dad1950f
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
- start-tauri-dev.mjs 在前端与配套后端就绪后、spawn Tauri CLI 之前调用准备步骤,并保留依赖注入供入口测试断言顺序 - build-release.mjs 的 runTauriBuild 与既有 stageRuntime 并列调用 stageBundledResources,tauri build --no-bundle 仍不强制 staging - 声明新增 origin 字段:source 由准备步骤写,build 由构建脚本在产物生成后写;构建脚本删除 codex 与插件白名单的写入分支及 stage_plugin_file/copy_plugin_tree/copy_plugin_file,改为 stage_build_generated_plugin_payloads - 插件随包工作区改为与仓库源码逐文件比对(清单 + 逐文件 sha256 + 整树符号链接),实现移入 build_support/package_layout.rs 复用单测 - 上游原生包元数据改由准备步骤按声明校验,build_support/codex_package_metadata.rs 因失去调用方删除 - 准备步骤改为同步实现,dev 与发布入口可直接调用而无需子进程 - 测试:build-release 39 passed(新增 stage、bundled、build 顺序与 no-bundle 不 staging)、dev 入口 12 passed(新增准备步骤先于 CLI 启动)、准备步骤 9 passed、package_layout 36 passed - 文档:技术方案 §4.9 记录构建期写入边界,M1 里程碑标记完成,运维文档补入口接线与 resources/plugins 所有权,决策日志与排障经验同步
285 lines
8.4 KiB
JavaScript
285 lines
8.4 KiB
JavaScript
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
|
|
import {
|
|
defaultEditorFeatures,
|
|
withDefaultCargoFeatures,
|
|
} from './cargo-features.mjs';
|
|
import {
|
|
readAgcDevEndpoint,
|
|
resolveAgcDevEndpoint,
|
|
withAgcDevEndpointEnv,
|
|
} from './dev-port.mjs';
|
|
import {
|
|
prepareBundledResources,
|
|
supportedHostTarget,
|
|
} from './prepare-bundled-resources.mjs';
|
|
import {
|
|
isAiGameCreatorServer,
|
|
preflightExistingVite,
|
|
readChildFailure,
|
|
readExistingViteServer,
|
|
spawnChild,
|
|
stopChild,
|
|
terminateChildTree,
|
|
waitForChildTermination,
|
|
} from './start-dev-stack.mjs';
|
|
|
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
const repoRoot = resolve(appRoot, '../..');
|
|
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
|
|
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
|
|
const designDebugEnabled =
|
|
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
|
|
|
|
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
|
const args = [...argv];
|
|
const configOverride = JSON.stringify({
|
|
build: { devUrl, beforeDevCommand: '' },
|
|
});
|
|
const separatorIndex = args.indexOf('--');
|
|
if (separatorIndex < 0) {
|
|
return ['dev', ...args, '--config', configOverride];
|
|
}
|
|
const separatedArguments = args.slice(separatorIndex);
|
|
if (separatedArguments[1] !== '--') {
|
|
separatedArguments.unshift('--');
|
|
}
|
|
return [
|
|
'dev',
|
|
...args.slice(0, separatorIndex),
|
|
'--config',
|
|
configOverride,
|
|
...separatedArguments,
|
|
];
|
|
}
|
|
|
|
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
|
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
|
function readDevCargoFeatures(env = process.env) {
|
|
const override = env.AGC_DEV_CARGO_FEATURES;
|
|
if (override !== undefined) {
|
|
return override
|
|
.split(',')
|
|
.map((value) => value.trim())
|
|
.filter(Boolean);
|
|
}
|
|
return defaultEditorFeatures(process.platform);
|
|
}
|
|
|
|
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
|
return withDefaultCargoFeatures(argv, features);
|
|
}
|
|
|
|
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
|
|
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
|
|
function prepareBundledResourcesBeforeTauri(
|
|
features = readDevCargoFeatures(),
|
|
{ prepare = prepareBundledResources, log = console.log } = {},
|
|
) {
|
|
const target = supportedHostTarget();
|
|
if (!target) {
|
|
log(
|
|
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
|
|
);
|
|
return;
|
|
}
|
|
const summaries = prepare({
|
|
target,
|
|
features: new Set(features),
|
|
log: (line) => log(`[ai-game-creator-shell] ${line}`),
|
|
});
|
|
for (const summary of summaries) {
|
|
log(`[ai-game-creator-shell] ${summary}`);
|
|
}
|
|
}
|
|
|
|
function spawnTauriCli(argv, { env = process.env } = {}) {
|
|
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
|
cwd: appRoot,
|
|
env,
|
|
shell: false,
|
|
});
|
|
}
|
|
|
|
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
|
|
/// `rustc-wrapper`(本地常见为 sccache)会在这里生效。本地 sccache daemon 状态
|
|
/// 一旦损坏,`cargo` 的首次 rustc 探测就会失败并阻断整个 AGC 启动;因此这里复用
|
|
/// `npm run dev` 的本地 Rust 环境规则,由脚本而不是本机 Cargo 配置决定 wrapper。
|
|
function buildTauriDevProcessEnv(endpoint, env = process.env) {
|
|
return buildLocalRustProcessEnv({
|
|
...withAgcDevEndpointEnv(endpoint, env),
|
|
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
|
});
|
|
}
|
|
|
|
async function runTauriDev(
|
|
argv = process.argv.slice(2),
|
|
{
|
|
resolveDevEndpoint = resolveAgcDevEndpoint,
|
|
preflight = preflightExistingVite,
|
|
prepareFrontend = prepareFrontendDev,
|
|
spawnCli = spawnTauriCli,
|
|
waitForCli = waitForChildTermination,
|
|
terminateTree = terminateChildTree,
|
|
prepareResources = prepareBundledResourcesBeforeTauri,
|
|
} = {},
|
|
) {
|
|
const endpoint = await resolveDevEndpoint();
|
|
await preflight({ endpoint });
|
|
|
|
let child = null;
|
|
let frontendChild = null;
|
|
const preparationAbort = new AbortController();
|
|
let resolveShutdown;
|
|
let shutdownSignal = '';
|
|
let repeatedSignal = false;
|
|
const shutdownRequested = new Promise((resolveRequest) => {
|
|
resolveShutdown = resolveRequest;
|
|
});
|
|
const signalHandlers = new Map();
|
|
|
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
const handler = () => {
|
|
if (!shutdownSignal) {
|
|
shutdownSignal = signal;
|
|
stopChild(child, 'SIGTERM');
|
|
stopChild(frontendChild, 'SIGTERM');
|
|
preparationAbort.abort();
|
|
resolveShutdown(signal);
|
|
return;
|
|
}
|
|
repeatedSignal = true;
|
|
stopChild(child, 'SIGKILL');
|
|
stopChild(frontendChild, 'SIGKILL');
|
|
};
|
|
signalHandlers.set(signal, handler);
|
|
process.on(signal, handler);
|
|
}
|
|
|
|
try {
|
|
const preparation = prepareFrontend(endpoint, {
|
|
signal: preparationAbort.signal,
|
|
onChild(frontend) {
|
|
frontendChild = frontend;
|
|
},
|
|
});
|
|
const prepared = await Promise.race([
|
|
preparation.then(() => true),
|
|
shutdownRequested.then(() => false),
|
|
]);
|
|
if (!prepared || shutdownSignal) return 1;
|
|
const devFeatures = readDevCargoFeatures();
|
|
prepareResources(devFeatures);
|
|
const tauriArguments = buildTauriArguments(
|
|
withDevCargoFeatures(argv, devFeatures),
|
|
endpoint.url,
|
|
);
|
|
child = spawnCli(tauriArguments, {
|
|
env: buildTauriDevProcessEnv(endpoint),
|
|
});
|
|
const childResult = waitForCli(child);
|
|
const outcome = await Promise.race([
|
|
childResult.then((failure) => ({ type: 'exit', failure })),
|
|
shutdownRequested.then((signal) => ({ type: 'signal', signal })),
|
|
...(frontendChild
|
|
? [
|
|
waitForChildTermination(frontendChild).then((failure) => ({
|
|
type: 'frontend-exit',
|
|
failure,
|
|
})),
|
|
]
|
|
: []),
|
|
]);
|
|
const cleanup = await terminateTree(child, {
|
|
gracefulTimeoutMs: repeatedSignal ? 0 : 2500,
|
|
});
|
|
if (!cleanup.stopped) {
|
|
console.error(
|
|
'[ai-game-creator-shell] Tauri dev exited, but its process tree could not be fully stopped.',
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
if (outcome.type === 'signal') {
|
|
return 1;
|
|
}
|
|
if (outcome.type === 'frontend-exit') return 1;
|
|
const { failure } = outcome;
|
|
return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0);
|
|
} finally {
|
|
for (const [signal, handler] of signalHandlers) {
|
|
process.off(signal, handler);
|
|
}
|
|
preparationAbort.abort();
|
|
if (frontendChild) {
|
|
const cleanup = await terminateTree(frontendChild);
|
|
if (!cleanup.stopped) {
|
|
console.error('[ai-game-creator-shell] 配套开发服务未能完全停止。');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function prepareFrontendDev(endpoint, { onChild, signal }) {
|
|
const frontend = spawnChild(
|
|
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
['run', 'agc:serve'],
|
|
{
|
|
cwd: repoRoot,
|
|
env: {
|
|
...withAgcDevEndpointEnv(endpoint),
|
|
},
|
|
},
|
|
);
|
|
onChild(frontend);
|
|
console.log(
|
|
'[ai-game-creator-shell] 正在准备前端与配套后端,完成后启动 Tauri',
|
|
);
|
|
const deadline = Date.now() + 660_000;
|
|
while (Date.now() < deadline) {
|
|
signal.throwIfAborted();
|
|
const failure = readChildFailure(frontend);
|
|
if (failure) {
|
|
throw new Error(
|
|
`配套开发服务退出,前端未就绪:${failure.error?.message ?? failure.signal ?? failure.code}`,
|
|
);
|
|
}
|
|
if (isAiGameCreatorServer(await readExistingViteServer(endpoint))) return;
|
|
await Promise.race([
|
|
new Promise((resolveWait) => setTimeout(resolveWait, 1000)),
|
|
waitForChildTermination(frontend),
|
|
]);
|
|
}
|
|
throw new Error(`等待前端与配套后端就绪超时:${endpoint.url}`);
|
|
}
|
|
|
|
function isDirectModuleExecution() {
|
|
return Boolean(
|
|
process.argv[1] &&
|
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
|
);
|
|
}
|
|
|
|
export {
|
|
buildTauriArguments,
|
|
buildTauriDevProcessEnv,
|
|
isDirectModuleExecution,
|
|
prepareBundledResourcesBeforeTauri,
|
|
runTauriDev,
|
|
spawnTauriCli,
|
|
withDevCargoFeatures,
|
|
};
|
|
|
|
if (isDirectModuleExecution()) {
|
|
try {
|
|
process.exitCode = await runTauriDev();
|
|
} catch (error) {
|
|
console.error(
|
|
`[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|