Files
Genarrative/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs
T
suzmii d2d78fa3d4
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 2m3s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / Native shell tests (pull_request) Failing after 1m44s
Project CI / Frontend tests (pull_request) Successful in 2m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m30s
修复AGC随包资源staging边界
修复 --no-bundle 构建前的资源准备接线。

拒绝插件随包目录中的未声明文件并同步Rust校验。

增加staging替换失败后的旧资源恢复与回归测试。
2026-09-30 19:12:30 +08:00

1756 lines
52 KiB
JavaScript
Executable File
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
// AGC 随包资源准备步骤:在 `tauri dev` / `tauri build` 之前把随包资源一次性 staging 到位。
//
// 合同见 docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md §4.3:
// 只替换本工具产物、写入临时目录后原子替换、命中缓存不重写任何文件、失败即关闭并给出可执行提示。
// 布局与组件白名单来自唯一声明 src-tauri/build_support/package-layout.json(Node 与 Rust 共用)。
//
// 本里程碑(M1)覆盖两条纯复制路径:随包 Codex CLI 与插件工作区。
// 编辑器分支产物(Unity/Godot/Cocos)仍由构建脚本生成,归位在 M3。
//
// 用法(在 apps/ai-game-creator-shell 下):
// node scripts/prepare-bundled-resources.mjs [--target <triple>] [--dry-run]
import { spawnSync } from 'node:child_process';
import { createHash, randomBytes } from 'node:crypto';
import {
chmodSync,
closeSync,
copyFileSync,
existsSync,
mkdirSync,
openSync,
readdirSync,
readFileSync,
readSync,
renameSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defaultEditorFeatures } from './cargo-features.mjs';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const APP_ROOT = path.resolve(SCRIPT_DIR, '..');
const REPO_ROOT = path.resolve(APP_ROOT, '..', '..');
/** 应用 src-tauri 目录(随包资源的落点)。 */
export const SRC_TAURI_DIR = path.join(APP_ROOT, 'src-tauri');
/** 唯一的随包资源声明文件。 */
export const DECLARATION_PATH = path.join(
SRC_TAURI_DIR,
'build_support/package-layout.json',
);
/** 缓存记录(放在 gitignored 的 target 下,丢失只多一次哈希)。 */
export const RECORD_PATH = path.join(
SRC_TAURI_DIR,
'target/agc-resource-staging.json',
);
const HOST_TRIPLES = new Map([
['darwin:arm64', 'aarch64-apple-darwin'],
['darwin:x64', 'x86_64-apple-darwin'],
['win32:x64', 'x86_64-pc-windows-msvc'],
]);
class PrepareError extends Error {}
function fail(message) {
throw new PrepareError(message);
}
/// 声明覆盖的宿主目标;不受声明覆盖的平台返回 null(入口据此跳过资源准备)。
export function supportedHostTarget(
platform = process.platform,
arch = process.arch,
) {
return HOST_TRIPLES.get(`${platform}:${arch}`) ?? null;
}
export function resolveHostTarget(
platform = process.platform,
arch = process.arch,
) {
const triple = supportedHostTarget(platform, arch);
if (!triple) {
fail(
`不支持的目标平台:${platform}/${arch}(随包资源声明只覆盖 ${[...HOST_TRIPLES.values()].join('、')})`,
);
}
return triple;
}
export function readDeclaration(declarationPath = DECLARATION_PATH) {
const declaration = JSON.parse(readFileSync(declarationPath, 'utf8'));
if (declaration.schema !== 'agc-package-layout.v1') {
fail(`不支持的随包资源声明 schema:${String(declaration.schema)}`);
}
return declaration;
}
function codexLayout(declaration, target) {
const layout = declaration.codex.targets.find(
(entry) => entry.target === target,
);
if (!layout) {
fail(`随包资源声明不含目标 ${target} 的 Codex 布局`);
}
return layout;
}
/// staging 的替换单位:属于整目录分组时替换整个分组目录(macOS 双架构共用),否则替换自身目录。
export function stagingUnit(declaration, target) {
const layout = codexLayout(declaration, target);
const group = declaration.codex.universalGroups.find((entry) =>
entry.targets.includes(target),
);
const directory = group ? group.directory : layout.directory;
return {
directory,
targets: (group ? group.targets : [target]).map((member) => ({
target: member,
layout: codexLayout(declaration, member),
})),
};
}
function sha256File(file) {
const hash = createHash('sha256');
const buffer = Buffer.allocUnsafe(64 * 1024);
const descriptor = openSync(file, 'r');
try {
for (;;) {
const read = readSync(descriptor, buffer, 0, buffer.length, null);
if (read === 0) {
break;
}
hash.update(buffer.subarray(0, read));
}
} finally {
closeSync(descriptor);
}
return hash.digest('hex');
}
function sha256Text(text) {
return createHash('sha256').update(text).digest('hex');
}
function copyFilePreservingMode(source, destination) {
const info = statSync(source);
if (!info.isFile()) {
fail(`随包资源来源不是普通文件:${source}`);
}
mkdirSync(path.dirname(destination), { recursive: true });
copyFileSync(source, destination);
chmodSync(destination, info.mode & 0o777);
}
export function readRecord(recordPath = RECORD_PATH) {
if (!existsSync(recordPath)) {
return {};
}
try {
return JSON.parse(readFileSync(recordPath, 'utf8'));
} catch {
return {};
}
}
function writeRecord(record, recordPath) {
mkdirSync(path.dirname(recordPath), { recursive: true });
writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
}
/// 上游平台包目录:按声明顺序取第一个「声明白名单文件齐全」的候选。
export function findCodexSource(declaration, target, roots) {
const layout = codexLayout(declaration, target);
const candidates = [];
for (const rootName of declaration.codex.sourceRoots) {
const root = roots[rootName];
if (!root) {
fail(`未知的声明 sourceRoots 取值:${rootName}`);
}
for (const relative of declaration.codex.sourceRelativePaths) {
candidates.push(
path.join(
root,
relative
.replaceAll('<platform>', layout.platform)
.replaceAll('<target>', target),
),
);
}
}
const source = candidates.find((candidate) =>
layout.files.every((entry) => existsSync(path.join(candidate, entry))),
);
if (!source) {
fail(
`内置 Codex CLI 上游包缺失;请先在仓库根目录执行 npm ci(已检查:${candidates.join(';')})`,
);
}
return source;
}
/// 上游原生包元数据必须与声明一致:版本、目标、入口、资源目录与 path 目录。
export function validateUpstreamMetadata(declaration, target, source) {
const metadataFile = path.join(
source,
declaration.codex.packageMetadataFileName,
);
const metadata = JSON.parse(readFileSync(metadataFile, 'utf8'));
const layout = codexLayout(declaration, target);
const expected = declaration.codex.packageMetadata;
const mismatch =
metadata.layoutVersion !== expected.layoutVersion ||
metadata.version !== declaration.codex.version ||
metadata.target !== target ||
metadata.entrypoint !== layout.executable ||
metadata.resourcesDir !== expected.resourcesDir ||
metadata.pathDir !== expected.pathDir;
if (mismatch) {
fail(
`内置 Codex CLI 上游包元数据与声明不一致(${metadataFile}):期望 layoutVersion=${expected.layoutVersion} version=${declaration.codex.version} target=${target} entrypoint=${layout.executable};请确认上游包版本后再同步 build_support/package-layout.json`,
);
}
}
/// 缓存 key 的锁定信息:上游 package-lock 的 resolved + integrity。
export function readLockedUpstream(
declaration,
target,
lockfilePath = path.join(REPO_ROOT, 'package-lock.json'),
) {
const layout = codexLayout(declaration, target);
const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf8'));
const entry =
lockfile.packages?.[`node_modules/@openai/codex-${layout.platform}`];
if (!entry?.resolved || !entry?.integrity) {
fail(
`package-lock.json 缺少 @openai/codex-${layout.platform} 的 resolved/integrity;请先在仓库根目录执行 npm ci`,
);
}
return { resolved: entry.resolved, integrity: entry.integrity };
}
function serializeManifest(declaration, layout, hashes) {
const files = {};
for (const relative of [...layout.files].sort()) {
files[relative] = hashes.get(relative);
}
return `${JSON.stringify(
{
files,
platform: layout.platform,
schemaVersion: declaration.codex.manifestSchema,
version: `${declaration.codex.cliVersionPrefix}${declaration.codex.version}`,
},
null,
2,
)}\n`;
}
function noticeSourceFor(declaration, target) {
const notice = declaration.codex.noticeSources.find((entry) =>
entry.targets.includes(target),
);
if (!notice) {
fail(`随包资源声明缺少目标 ${target} 的第三方声明来源`);
}
return notice;
}
function unitPathOf(destinationRoot, declaration, unit) {
return path.join(
destinationRoot,
declaration.codex.resourceDirectory,
unit.directory,
);
}
function targetPathWithin(unitPath, declaration, unit, target) {
const layout = codexLayout(declaration, target);
const relative = path.relative(unit.directory, layout.directory);
return relative ? path.join(unitPath, relative) : unitPath;
}
/// 该替换单位允许出现的顶层条目:整目录分组下是各架构子目录,单目标下是组件首段路径 + 第三方声明 + 清单。
function allowedUnitEntries(declaration, unit) {
const allowed = new Set();
for (const member of unit.targets) {
const relativeDirectory = path.relative(
unit.directory,
member.layout.directory,
);
if (relativeDirectory) {
allowed.add(relativeDirectory.split(path.sep)[0]);
continue;
}
for (const relative of member.layout.files) {
allowed.add(relative.split('/')[0]);
}
allowed.add(declaration.codex.noticeFileName);
allowed.add(declaration.codex.manifestFileName);
}
return allowed;
}
function assertOwnedUnit(unitPath, declaration, unit) {
if (!existsSync(unitPath)) {
return;
}
const allowed = allowedUnitEntries(declaration, unit);
for (const entry of readdirSync(unitPath)) {
const entryPath = path.join(unitPath, entry);
if (!allowed.has(entry)) {
fail(
`随包资源目录被非本工具内容占用:${entryPath};请人工确认后删除该目录再重试`,
);
}
}
}
/// 期望产物:每个目标的组件摘要与清单文本。
function desiredCodex(declaration, unit, roots) {
const desired = new Map();
for (const member of unit.targets) {
const source = findCodexSource(declaration, member.target, roots);
validateUpstreamMetadata(declaration, member.target, source);
const hashes = new Map();
for (const relative of member.layout.files) {
hashes.set(relative, sha256File(path.join(source, relative)));
}
desired.set(member.target, {
source,
files: new Map(
[...hashes].map(([relative, digest]) => [
relative,
{ sha256: digest, size: statSync(path.join(source, relative)).size },
]),
),
manifest: serializeManifest(declaration, member.layout, hashes),
});
}
return desired;
}
function unitMatchesExisting(unitPath, declaration, unit, desired) {
if (!existsSync(unitPath)) {
return false;
}
const allowed = allowedUnitEntries(declaration, unit);
for (const entry of readdirSync(unitPath)) {
if (!allowed.has(entry)) {
return false;
}
}
for (const member of unit.targets) {
const expected = desired.get(member.target);
const targetDir = targetPathWithin(
unitPath,
declaration,
unit,
member.target,
);
const manifestPath = path.join(
targetDir,
declaration.codex.manifestFileName,
);
if (
!existsSync(manifestPath) ||
readFileSync(manifestPath, 'utf8') !== expected.manifest
) {
return false;
}
for (const [relative, file] of expected.files) {
const filePath = path.join(targetDir, relative);
if (!existsSync(filePath)) {
return false;
}
const info = statSync(filePath);
if (info.size !== file.size || sha256File(filePath) !== file.sha256) {
return false;
}
if (relative === member.layout.executable && (info.mode & 0o111) === 0) {
return false;
}
}
}
return true;
}
function recordEntryFor(unitPath, declaration, unit, desired) {
const manifests = {};
const sizes = {};
for (const member of unit.targets) {
const expected = desired.get(member.target);
const targetDir = targetPathWithin(
unitPath,
declaration,
unit,
member.target,
);
manifests[member.target] = sha256Text(expected.manifest);
sizes[member.layout.directory] = Object.fromEntries(
[...expected.files].map(([relative, file]) => [relative, file.size]),
);
void targetDir;
}
return { manifests, sizes };
}
function unitRecordMatches(unitPath, declaration, unit, recorded) {
if (!recorded) {
return false;
}
for (const member of unit.targets) {
const targetDir = targetPathWithin(
unitPath,
declaration,
unit,
member.target,
);
const manifestPath = path.join(
targetDir,
declaration.codex.manifestFileName,
);
if (!existsSync(manifestPath)) {
return false;
}
if (
sha256Text(readFileSync(manifestPath, 'utf8')) !==
recorded.manifests?.[member.target]
) {
return false;
}
const expectedSizes = recorded.sizes?.[member.layout.directory];
if (!expectedSizes) {
return false;
}
for (const relative of member.layout.files) {
const file = path.join(targetDir, relative);
if (
!existsSync(file) ||
statSync(file).size !== expectedSizes[relative]
) {
return false;
}
}
}
return true;
}
function buildUnitInto(
stagingPath,
declaration,
unit,
desired,
unitPath,
destinationRoot,
) {
for (const member of unit.targets) {
const expected = desired.get(member.target);
const targetDir = targetPathWithin(
stagingPath,
declaration,
unit,
member.target,
);
for (const relative of member.layout.files) {
copyFilePreservingMode(
path.join(expected.source, relative),
path.join(targetDir, relative),
);
}
const notice = noticeSourceFor(declaration, member.target);
const noticeDestination = path.join(
targetDir,
declaration.codex.noticeFileName,
);
const noticeSourcePath = notice.preserve
? path.join(
targetPathWithin(unitPath, declaration, unit, member.target),
declaration.codex.noticeFileName,
)
: path.join(destinationRoot, notice.source);
if (!existsSync(noticeSourcePath)) {
fail(`第三方声明来源缺失:${noticeSourcePath}`);
}
copyFilePreservingMode(noticeSourcePath, noticeDestination);
writeFileSync(
path.join(targetDir, declaration.codex.manifestFileName),
expected.manifest,
);
}
}
function stageAtomically(unitPath, builder) {
const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
const stagingPath = `${unitPath}-staging-${suffix}`;
const backupPath = `${unitPath}-backup-${suffix}`;
rmSync(stagingPath, { recursive: true, force: true });
rmSync(backupPath, { recursive: true, force: true });
mkdirSync(stagingPath, { recursive: true });
try {
builder(stagingPath);
} catch (error) {
rmSync(stagingPath, { recursive: true, force: true });
throw error;
}
const hadPrevious = existsSync(unitPath);
try {
if (hadPrevious) {
renameSync(unitPath, backupPath);
}
renameSync(stagingPath, unitPath);
} catch (error) {
let restored = !hadPrevious;
if (hadPrevious && existsSync(backupPath)) {
try {
if (existsSync(unitPath)) {
rmSync(unitPath, { recursive: true, force: true });
}
renameSync(backupPath, unitPath);
restored = true;
} catch {
restored = false;
}
}
fail(
`替换 ${unitPath} 失败(${error.code ?? error.message}):staging 保留在 ${stagingPath};旧资源${restored ? '已恢复' : '恢复失败,请检查 ' + backupPath},确认没有并发进程后重试`,
);
}
rmSync(backupPath, { recursive: true, force: true });
}
function prepareCodex({
declaration,
target,
destinationRoot,
roots,
lockfilePath,
record,
dryRun,
}) {
const unit = stagingUnit(declaration, target);
const label = `codex ${unit.targets.map((member) => member.target).join('+')}`;
const unitPath = unitPathOf(destinationRoot, declaration, unit);
const key = [
`layoutVersion=${declaration.layoutVersion}`,
...unit.targets.map(
(member) =>
`${member.target}:${readLockedUpstream(declaration, member.target, lockfilePath).integrity}`,
),
].join('|');
const recorded = record.codex?.[unit.directory];
if (
recorded?.key === key &&
unitRecordMatches(unitPath, declaration, unit, recorded)
) {
return { summary: `${label} 命中缓存(未写入)`, record: undefined };
}
const desired = desiredCodex(declaration, unit, roots);
const entry = {
key,
...recordEntryFor(unitPath, declaration, unit, desired),
};
if (unitMatchesExisting(unitPath, declaration, unit, desired)) {
return { summary: `${label} 命中缓存(内容一致,未写入)`, record: entry };
}
if (dryRun) {
return {
summary: `${label} 需要重新生成(dry-run 未写入)`,
record: undefined,
};
}
assertOwnedUnit(unitPath, declaration, unit);
stageAtomically(unitPath, (staging) =>
buildUnitInto(
staging,
declaration,
unit,
desired,
unitPath,
destinationRoot,
),
);
return {
summary: `${label} 重新生成(${unit.targets.length} 个架构,写入 ${declaration.codex.resourceDirectory}/${unit.directory})`,
record: entry,
};
}
/// Claude Agent SDK sidecar 的声明目标;未声明的目标不随包 sidecar(Linux 等平台直接跳过)。
function claudeAgentLayout(declaration, target) {
return (
declaration.claudeAgent.targets.find((entry) => entry.target === target) ??
null
);
}
/// 上游包目录:按声明顺序在 app / 仓库根的 node_modules 下取第一个命中的候选。
function requireClaudeAgentPackage(declaration, roots, packageName, label) {
const candidates = declaration.claudeAgent.sourceRoots.map((rootName) => {
const root = roots[rootName];
if (!root) {
fail(`未知的声明 sourceRoots 取值:${rootName}`);
}
return path.join(
root,
declaration.claudeAgent.nodeModulesDirectory,
packageName,
);
});
const found = candidates.find((candidate) => existsSync(candidate));
if (!found) {
fail(
`${label}缺失;请先在仓库根目录执行 npm ci(已检查:${candidates.join(';')})`,
);
}
return found;
}
/// 上游包版本必须与声明一致:静默发旧 SDK 的代价是 sidecar 与客户端说不上话。
function validateClaudeAgentPackageVersion(packageDirectory, declared, label) {
const metadataFile = path.join(
packageDirectory,
declared.sdkPackageMetadataFileName,
);
let metadata;
try {
metadata = JSON.parse(readFileSync(metadataFile, 'utf8'));
} catch (error) {
fail(
`${label} 元数据不可读或不是合法 JSON:${metadataFile}(${error.message})`,
);
}
if (metadata.version !== declared.version) {
fail(
`${label} 版本与声明不一致:${String(metadata.version)}(期望 ${declared.version},${metadataFile});请同步更新 build_support/package-layout.json`,
);
}
}
/// 期望产物:仓库里的 sidecar 入口 + 上游 SDK 包 + 平台原生运行时包(随包相对路径 → 来源文件)。
function claudeAgentDesiredFiles(declaration, layout, roots) {
const declared = declaration.claudeAgent;
const scope = declared.sdkPackageName.split('/')[0];
const entrySource = path.join(roots.app, declared.entryRelativePath);
if (!existsSync(entrySource)) {
fail(`Claude Agent SDK sidecar 入口缺失:${entrySource}`);
}
const sdkSource = requireClaudeAgentPackage(
declaration,
roots,
declared.sdkPackageName,
'Claude Agent SDK 上游包',
);
if (!existsSync(path.join(sdkSource, declared.sdkEntryFileName))) {
fail(
`Claude Agent SDK 上游包缺少入口 ${declared.sdkEntryFileName}:${sdkSource}`,
);
}
const runtimePackage = `${scope}/${layout.runtimePackage}`;
const runtimeSource = requireClaudeAgentPackage(
declaration,
roots,
runtimePackage,
`Claude Agent SDK 原生运行时(${layout.runtimePackage})`,
);
if (!existsSync(path.join(runtimeSource, layout.runtimeFileName))) {
fail(
`Claude Agent SDK 原生运行时缺少可执行文件:${path.join(runtimeSource, layout.runtimeFileName)}`,
);
}
validateClaudeAgentPackageVersion(sdkSource, declared, 'Claude Agent SDK');
validateClaudeAgentPackageVersion(
runtimeSource,
declared,
'Claude Agent SDK 原生运行时',
);
const files = new Map([
[
declared.entryFileName,
{
source: entrySource,
executable: false,
},
],
]);
for (const relative of walkFiles(
declared,
sdkSource,
'Claude Agent SDK 上游包',
)) {
files.set(
`${declared.nodeModulesDirectory}/${declared.sdkPackageName}/${relative}`,
{
source: path.join(sdkSource, relative),
executable: false,
},
);
}
for (const relative of walkFiles(
declared,
runtimeSource,
'Claude Agent SDK 原生运行时',
)) {
files.set(
`${declared.nodeModulesDirectory}/${runtimePackage}/${relative}`,
{
source: path.join(runtimeSource, relative),
executable: relative === layout.runtimeFileName,
},
);
}
return files;
}
/// 已落盘目录里的全部普通文件(相对路径);不套用复制规则,越界文件必须能被看见。
function collectClaudeAgentFiles(root) {
const files = [];
const stack = [['', root]];
while (stack.length > 0) {
const [prefix, directory] = stack.pop();
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
const entryPath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
fail(`Claude Agent SDK 随包资源不允许符号链接:${entryPath}`);
}
if (entry.isDirectory()) {
stack.push([relative, entryPath]);
} else if (entry.isFile()) {
files.push(relative);
}
}
}
return files.sort();
}
/// 随包目录与期望产物是否一致:路径集合、尺寸、可执行位,必要时再逐文件比对内容。
function claudeAgentTreeMatches(unitPath, files, { compareContent }) {
if (!existsSync(unitPath)) {
return false;
}
const expected = [...files.keys()].sort();
const actual = collectClaudeAgentFiles(unitPath);
if (
actual.length !== expected.length ||
actual.some((relative, index) => relative !== expected[index])
) {
return false;
}
for (const [relative, entry] of files) {
const staged = path.join(unitPath, ...relative.split('/'));
const sourceInfo = statSync(entry.source);
const stagedInfo = statSync(staged);
if (sourceInfo.size !== stagedInfo.size) {
return false;
}
// Windows 的 mode 位不表达可执行语义;其它平台要求声明为可执行的文件确实带可执行位。
if (
process.platform !== 'win32' &&
entry.executable &&
(stagedInfo.mode & 0o111) === 0
) {
return false;
}
if (compareContent && !sameFileContent(entry.source, staged)) {
return false;
}
}
return true;
}
/// 只允许替换由本工具创建的目录:出现白名单外的顶层条目即失败关闭。
function assertOwnedClaudeAgentRoot(unitPath, declared) {
if (!existsSync(unitPath)) {
return;
}
const allowed = new Set([
declared.entryFileName,
declared.nodeModulesDirectory,
]);
for (const entry of readdirSync(unitPath)) {
if (!allowed.has(entry)) {
fail(
`Claude Agent SDK sidecar 随包目录被非本工具内容占用:${path.join(unitPath, entry)};请人工确认后删除该目录再重试`,
);
}
}
}
/// Claude Agent SDK sidecar:入口来自仓库源码、SDK 与原生运行时来自上游 npm 包。
/// 整目录原子替换;命中缓存时不写任何文件。缓存 key = 声明版本 + 目标 + 入口摘要,
/// 上游包按 npm 版本锁定,因此快速路径只比路径集合、尺寸与可执行位(与 codex 的缓存口径一致)。
function prepareClaudeAgent({
declaration,
target,
destinationRoot,
roots,
record,
dryRun,
}) {
const layout = claudeAgentLayout(declaration, target);
if (!layout) {
return {
summary: `claude-agent 跳过(目标 ${target} 不适用)`,
record: undefined,
};
}
const declared = declaration.claudeAgent;
const label = `claude-agent ${target}`;
const unitPath = path.join(destinationRoot, declared.resourceDirectory);
const files = claudeAgentDesiredFiles(declaration, layout, roots);
const key = [
`layoutVersion=${declaration.layoutVersion}`,
`target=${target}`,
`version=${declared.version}`,
`entry=${sha256File(path.join(roots.app, declared.entryRelativePath))}`,
].join('|');
assertOwnedClaudeAgentRoot(unitPath, declared);
if (
record.claudeAgent?.[target]?.key === key &&
claudeAgentTreeMatches(unitPath, files, { compareContent: false })
) {
return { summary: `${label} 命中缓存(未写入)`, record: undefined };
}
if (claudeAgentTreeMatches(unitPath, files, { compareContent: true })) {
return {
summary: `${label} 命中缓存(内容一致,未写入)`,
record: { key },
};
}
if (dryRun) {
return {
summary: `${label} 需要重新生成(dry-run 未写入)`,
record: undefined,
};
}
stageAtomically(unitPath, (staging) => {
for (const [relative, entry] of files) {
const destination = path.join(staging, ...relative.split('/'));
copyFilePreservingMode(entry.source, destination);
// 原生运行时必须可执行:上游 tarball 的权限位偶有丢失,这里按声明兜底补一次。
if (entry.executable && process.platform !== 'win32') {
chmodSync(destination, 0o755);
}
}
});
return {
summary: `${label} 重新生成(${files.size} 个文件,写入 ${declared.resourceDirectory})`,
record: { key },
};
}
export function pluginDirectories(declaration, repoRoot) {
const root = path.join(repoRoot, declaration.plugins.sourceDirectory);
if (!existsSync(root)) {
fail(`插件工作区缺失:${root}`);
}
return readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
.map((entry) => ({ name: entry.name, root: path.join(root, entry.name) }))
.filter((plugin) =>
existsSync(path.join(plugin.root, declaration.plugins.manifestFileName)),
);
}
function subdirectoryAppliesToPlugin(subdirectory, pluginName) {
return !subdirectory.plugin || subdirectory.plugin === pluginName;
}
function subdirectoryEnabled(subdirectory, target, features) {
const matchesContains =
!subdirectory.targetContains?.length ||
subdirectory.targetContains.some((needle) => target.includes(needle));
const matchesTarget =
!subdirectory.targets?.length || subdirectory.targets.includes(target);
const matchesFeatures = (subdirectory.features ?? []).every((name) =>
features.has(name),
);
return matchesContains && matchesTarget && matchesFeatures;
}
function collectTreeFiles(root, label) {
const files = [];
const stack = [['', root]];
while (stack.length > 0) {
const [prefix, directory] = stack.pop();
if (!existsSync(directory)) {
continue;
}
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
const entryPath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
fail(`${label}不允许符号链接:${entryPath}`);
}
if (entry.isDirectory()) {
stack.push([relative, entryPath]);
} else if (entry.isFile()) {
files.push(relative);
}
}
}
return files.sort();
}
/// 复制规则由各 section 自带(plugins / claudeAgent),同名语义、同序判定。
function skipDirectory(rules, name) {
return (
rules.skipDirectoryNames.includes(name) ||
rules.skipDirectoryNamePrefixes.some((prefix) => name.startsWith(prefix))
);
}
function skipFileName(rules, name) {
return (
rules.skipFileNamePrefixes.some((prefix) => name.startsWith(prefix)) ||
rules.skipFileNameFragments.some((fragment) => name.includes(fragment))
);
}
function walkFiles(rules, root, label) {
const files = [];
const stack = [['', root]];
while (stack.length > 0) {
const [prefix, directory] = stack.pop();
if (!existsSync(directory)) {
continue;
}
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
const entryPath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
fail(`${label}不允许符号链接:${entryPath}`);
}
if (entry.isDirectory()) {
if (!skipDirectory(rules, entry.name)) {
stack.push([relative, entryPath]);
}
} else if (entry.isFile() && !skipFileName(rules, entry.name)) {
files.push(relative);
}
}
}
return files.sort();
}
function copyPluginSubdirectory(declaration, source, destination) {
if (!existsSync(source)) {
return;
}
mkdirSync(destination, { recursive: true });
for (const entry of readdirSync(source, { withFileTypes: true })) {
const entrySource = path.join(source, entry.name);
const entryDestination = path.join(destination, entry.name);
if (entry.isSymbolicLink()) {
fail(`插件资源不允许符号链接:${entrySource}`);
}
if (entry.isDirectory()) {
if (!skipDirectory(declaration.plugins, entry.name)) {
copyPluginSubdirectory(declaration, entrySource, entryDestination);
}
} else if (
entry.isFile() &&
!skipFileName(declaration.plugins, entry.name)
) {
copyFilePreservingMode(entrySource, entryDestination);
}
}
}
function pluginSourceFingerprint(declaration, plugins, target, features) {
const lines = [];
for (const plugin of plugins) {
for (const subdirectory of declaration.plugins.subdirectories) {
if (
!subdirectoryAppliesToPlugin(subdirectory, plugin.name) ||
!subdirectoryEnabled(subdirectory, target, features)
) {
continue;
}
const root = path.join(plugin.root, subdirectory.path);
for (const file of walkFiles(declaration.plugins, root, '插件资源')) {
const absolute = path.join(root, file);
const info = statSync(absolute);
// prepared 产物由准备步骤无条件复制,mtime 会变;用内容哈希保证幂等判断稳定。
const stamp =
subdirectory.origin === 'prepared'
? `sha256:${sha256File(absolute)}`
: `${info.size}:${Math.round(info.mtimeMs)}`;
lines.push(`${plugin.name}/${subdirectory.path}/${file}:${stamp}`);
}
}
const manifest = path.join(
plugin.root,
declaration.plugins.manifestFileName,
);
const info = statSync(manifest);
lines.push(
`${plugin.name}/plugin.json:${info.size}:${Math.round(info.mtimeMs)}`,
);
for (const staging of declaration.plugins.libraryStaging ?? []) {
if (
plugin.name !== staging.plugin ||
!libraryStagingEnabled(staging, target, features)
) {
continue;
}
for (const relative of staging.files) {
const file = path.join(
plugin.root,
staging.sourceSubdirectory,
relative,
);
lines.push(
`${plugin.name}/${staging.sourceSubdirectory}/${relative}:${
existsSync(file) ? `sha256:${sha256File(file)}` : 'missing'
}`,
);
}
}
for (const payload of declaration.plugins.nativePayloads ?? []) {
if (
plugin.name !== payload.plugin ||
!nativePayloadEnabled(payload, target, features)
) {
continue;
}
const delivered = path.join(
plugin.root,
payload.destinationSubdirectory,
payload.destinationFileName ?? payload.sourceFileName,
);
lines.push(
`${plugin.name}/${payload.destinationSubdirectory}:${
existsSync(delivered) ? `sha256:${sha256File(delivered)}` : 'missing'
}`,
);
}
}
return sha256Text(lines.join('\n'));
}
function pluginTreeMatches(
declaration,
plugins,
target,
features,
destination,
) {
if (!existsSync(destination)) {
return false;
}
const allowedFiles = new Set();
for (const plugin of plugins) {
const pluginDestination = path.join(destination, plugin.name);
const pluginPrefix = `${plugin.name}/`;
allowedFiles.add(`${pluginPrefix}${declaration.plugins.manifestFileName}`);
if (
!existsSync(
path.join(pluginDestination, declaration.plugins.manifestFileName),
)
) {
return false;
}
for (const subdirectory of declaration.plugins.subdirectories) {
if (!subdirectoryAppliesToPlugin(subdirectory, plugin.name)) {
continue;
}
const stagedDirectory = path.join(pluginDestination, subdirectory.path);
if (!subdirectoryEnabled(subdirectory, target, features)) {
if (existsSync(stagedDirectory)) {
return false;
}
continue;
}
const relativePrefix = `${plugin.name}/${subdirectory.path}/`;
const sourceRoot = path.join(plugin.root, subdirectory.path);
if (subdirectory.origin !== 'source') {
if (existsSync(sourceRoot) && !existsSync(stagedDirectory)) {
return false;
}
for (const relative of walkFiles(
declaration.plugins,
sourceRoot,
'插件资源',
)) {
allowedFiles.add(`${relativePrefix}${relative}`);
}
continue;
}
for (const relative of walkFiles(
declaration.plugins,
sourceRoot,
'插件资源',
)) {
const source = path.join(sourceRoot, relative);
const staged = path.join(
pluginDestination,
subdirectory.path,
relative,
);
allowedFiles.add(`${relativePrefix}${relative}`);
if (!existsSync(staged)) {
return false;
}
if (statSync(source).size !== statSync(staged).size) {
return false;
}
if (sha256File(source) !== sha256File(staged)) {
return false;
}
}
}
for (const staging of declaration.plugins.libraryStaging ?? []) {
if (
plugin.name === staging.plugin &&
libraryStagingEnabled(staging, target, features)
) {
for (const relative of staging.files) {
allowedFiles.add(
`${plugin.name}/${staging.sourceSubdirectory}/${relative}`,
);
}
}
}
for (const payload of declaration.plugins.nativePayloads ?? []) {
if (
plugin.name === payload.plugin &&
nativePayloadEnabled(payload, target, features)
) {
allowedFiles.add(
`${plugin.name}/${payload.destinationSubdirectory}/${payload.destinationFileName ?? payload.sourceFileName}`,
);
}
}
}
return collectTreeFiles(destination, '插件随包目录').every((relative) =>
allowedFiles.has(relative),
);
}
function assertOwnedPluginRoot(destination, plugins) {
if (!existsSync(destination)) {
return;
}
const known = new Set(plugins.map((plugin) => plugin.name));
for (const entry of readdirSync(destination)) {
if (!known.has(entry)) {
fail(
`插件随包目录被非本工具内容占用:${path.join(destination, entry)};请人工确认后删除该目录再重试`,
);
}
}
}
/// 声明的准备步骤:需要外部工具链或同一次 cargo 构建才能产出的随包内容。
function prepareStepsReferencedBy(declaration, target, features) {
const required = new Set();
for (const subdirectory of declaration.plugins.subdirectories) {
if (
subdirectory.origin === 'prepared' &&
subdirectoryEnabled(subdirectory, target, features)
) {
required.add(subdirectory.prepare);
}
}
for (const staging of declaration.plugins.libraryStaging ?? []) {
if (libraryStagingEnabled(staging, target, features)) {
required.add(staging.prepare);
}
}
for (const payload of declaration.plugins.nativePayloads ?? []) {
if (nativePayloadEnabled(payload, target, features)) {
required.add(payload.prepare);
}
}
return required;
}
function libraryStagingEnabled(staging, target, features) {
return (
(!staging.targets?.length || staging.targets.includes(target)) &&
(staging.features ?? []).every((name) => features.has(name))
);
}
function nativePayloadEnabled(payload, target, features) {
return (
(!payload.targets?.length || payload.targets.includes(target)) &&
(payload.features ?? []).every((name) => features.has(name))
);
}
/// 指纹覆盖声明的根目录(跳过排除目录),与构建脚本原先的规则一致:按相对路径排序后逐文件哈希。
function fingerprintSources(repoRoot, step) {
const { roots, excludeDirectoryNames } = step.fingerprint;
const lines = [];
for (const root of roots) {
const absoluteRoot = path.join(repoRoot, step.workingDirectory, root);
for (const relative of collectFingerprintFiles(
absoluteRoot,
excludeDirectoryNames,
)) {
const file = path.join(absoluteRoot, relative);
const info = statSync(file);
lines.push(`${relative}\u0000${info.size}\u0000${sha256File(file)}`);
}
}
return sha256Text(lines.join('\n'));
}
function collectFingerprintFiles(root, excludeDirectoryNames, prefix = '') {
const files = [];
if (!existsSync(root)) {
return files;
}
for (const entry of readdirSync(root, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isSymbolicLink()) {
fail(`准备步骤源码不允许符号链接:${path.join(root, entry.name)}`);
}
if (entry.isDirectory()) {
if (!excludeDirectoryNames.includes(entry.name)) {
files.push(
...collectFingerprintFiles(
path.join(root, entry.name),
excludeDirectoryNames,
relative,
),
);
}
} else if (entry.isFile()) {
files.push(relative);
}
}
return files.sort();
}
function requiredOutputsPresent(repoRoot, step) {
return step.requiredOutputs.every((relative) => {
const file = path.join(repoRoot, relative);
return (
existsSync(file) && statSync(file).isFile() && statSync(file).size > 0
);
});
}
function defaultRunCommand({ program, args, cwd, removeEnvironment }) {
const environment = { ...process.env };
for (const name of removeEnvironment ?? []) {
delete environment[name];
}
const result = spawnSync(program, args, {
cwd,
env: environment,
stdio: 'inherit',
});
if (result.error) {
throw new PrepareError(`执行 ${program} 失败:${result.error.message}`);
}
if (result.status !== 0) {
throw new PrepareError(`${program} 退出码 ${result.status}`);
}
}
/// 按声明产出「已准备」随包内容;命中指纹且必需产物齐全时零写入。
export function ensurePreparedArtifacts({
declaration,
repoRoot,
srcTauriRoot,
target,
features,
profile = 'debug',
runCommand = defaultRunCommand,
log = () => {},
dryRun = false,
}) {
const required = prepareStepsReferencedBy(declaration, target, features);
const steps = new Map(
(declaration.plugins.prepareSteps ?? []).map((step) => [step.name, step]),
);
for (const name of required) {
const step = steps.get(name);
if (!step) {
fail(`声明引用了未定义的准备步骤:${name}`);
}
const stampPath = step.fingerprint
? path.join(
repoRoot,
step.workingDirectory,
step.fingerprint.stampRelativePath,
)
: undefined;
if (step.fingerprint && step.requiredOutputs.length > 0) {
const expected = fingerprintSources(repoRoot, step);
const current = existsSync(stampPath)
? readFileSync(stampPath, 'utf8').trim()
: '';
if (current === expected && requiredOutputsPresent(repoRoot, step)) {
log(`${name} 命中缓存(指纹一致)`);
continue;
}
}
if (dryRun) {
log(`${name} 需要重新构建(dry-run 未执行)`);
continue;
}
if (step.kind === 'powershell') {
runCommand({
program: 'powershell.exe',
args: [
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-File',
path.join(repoRoot, step.workingDirectory, step.scriptFileName),
],
cwd: path.join(repoRoot, step.workingDirectory),
removeEnvironment: step.removeEnvironment ?? [],
});
} else if (step.kind === 'cargo') {
runCommand({
program: 'cargo',
args: [
'build',
'--manifest-path',
path.join(repoRoot, step.packageDirectory, 'Cargo.toml'),
'--target-dir',
path.join(srcTauriRoot, 'target'),
'--target',
target,
...(profile === 'release' ? ['--release'] : []),
...(step.features?.length
? ['--features', step.features.join(',')]
: []),
],
cwd: repoRoot,
removeEnvironment: [],
});
} else {
fail(`未实现的准备步骤类型:${step.kind}`);
}
if (!requiredOutputsPresent(repoRoot, step)) {
const missing = step.requiredOutputs.filter(
(relative) => !existsSync(path.join(repoRoot, relative)),
);
fail(`${name} 未产出必需文件:${missing.join('、') || '(未知)'}`);
}
if (stampPath && step.fingerprint) {
mkdirSync(path.dirname(stampPath), { recursive: true });
writeFileSync(stampPath, `${fingerprintSources(repoRoot, step)}\n`);
}
log(`${name} 已构建`);
}
}
/// 复制声明为已准备的随包子目录与随包库。
function copyPreparedPayloads({
declaration,
repoRoot,
staging,
target,
features,
}) {
for (const plugin of pluginDirectories(declaration, repoRoot)) {
for (const subdirectory of declaration.plugins.subdirectories) {
if (
subdirectory.origin !== 'prepared' ||
!subdirectoryEnabled(subdirectory, target, features)
) {
continue;
}
copyPluginSubdirectory(
declaration,
path.join(plugin.root, subdirectory.path),
path.join(staging, plugin.name, subdirectory.path),
);
}
for (const stagingEntry of declaration.plugins.libraryStaging ?? []) {
if (
plugin.name !== stagingEntry.plugin ||
!libraryStagingEnabled(stagingEntry, target, features)
) {
continue;
}
for (const relative of stagingEntry.files) {
copyFilePreservingMode(
path.join(plugin.root, stagingEntry.sourceSubdirectory, relative),
path.join(
staging,
plugin.name,
stagingEntry.sourceSubdirectory,
relative,
),
);
}
}
}
}
/// 逐字节相等且尺寸一致;目标不存在即视为不同。
function sameFileContent(source, destination) {
if (!existsSync(destination)) {
return false;
}
const sourceInfo = statSync(source);
const destinationInfo = statSync(destination);
return (
sourceInfo.size === destinationInfo.size &&
Buffer.compare(readFileSync(source), readFileSync(destination)) === 0
);
}
/// 内容一致时不重写(保住时间戳与「命中缓存零写入」口径)。
function copyFilePreservingModeIfChanged(source, destination) {
if (sameFileContent(source, destination)) {
return;
}
copyFilePreservingMode(source, destination);
}
/// Cocos bridge 的 dll 既要进插件工作区(唯一真源),也要进随包目录。
function copyNativePayloads({
declaration,
repoRoot,
srcTauriRoot,
staging,
target,
features,
profile,
}) {
for (const payload of declaration.plugins.nativePayloads ?? []) {
if (!nativePayloadEnabled(payload, target, features)) {
continue;
}
const candidates = [
path.join(
srcTauriRoot,
'target',
target,
profile,
'deps',
payload.sourceFileName,
),
path.join(
srcTauriRoot,
'target',
target,
profile,
payload.sourceFileName,
),
path.join(
srcTauriRoot,
'target',
profile,
'deps',
payload.sourceFileName,
),
path.join(srcTauriRoot, 'target', profile, payload.sourceFileName),
];
const source = candidates.find((candidate) => existsSync(candidate));
if (!source) {
fail(
`Cocos bridge native payload 未构建:${candidates.map((candidate) => path.relative(repoRoot, candidate)).join(';')}`,
);
}
const destinationName =
payload.destinationFileName ?? payload.sourceFileName;
copyFilePreservingModeIfChanged(
source,
path.join(
repoRoot,
'plugins',
payload.plugin,
payload.destinationSubdirectory,
destinationName,
),
);
copyFilePreservingModeIfChanged(
source,
path.join(
staging,
payload.plugin,
payload.destinationSubdirectory,
destinationName,
),
);
}
}
function preparePlugins({
declaration,
target,
destinationRoot,
repoRoot,
features,
plugins,
record,
profile,
dryRun,
}) {
const destination = path.join(
destinationRoot,
declaration.plugins.destinationDirectory,
);
const fingerprint = pluginSourceFingerprint(
declaration,
plugins,
target,
features,
);
const upToDate =
record.plugins?.fingerprint === fingerprint &&
pluginTreeMatches(declaration, plugins, target, features, destination);
// dry-run 必须零写入:只做只读判断就返回。
if (dryRun) {
return {
summary: upToDate
? `plugins 命中缓存(未写入,${plugins.length} 个插件)`
: `plugins 需要重新生成(dry-run 未写入,${plugins.length} 个插件)`,
record: undefined,
};
}
// 所有权断言先于任何写入(含下面的预缓存交付)。
assertOwnedPluginRoot(destination, plugins);
// payload 交付不能排在缓存短路之后:否则「先默认构建、之后开 injection」会把交付整条跳过。
if (existsSync(destination)) {
copyNativePayloads({
declaration,
repoRoot,
srcTauriRoot: destinationRoot,
staging: destination,
target,
features,
profile,
});
}
if (upToDate) {
return {
summary: `plugins 命中缓存(未写入,${plugins.length} 个插件)`,
record: undefined,
};
}
stageAtomically(destination, (staging) => {
for (const plugin of plugins) {
const pluginDestination = path.join(staging, plugin.name);
copyFilePreservingMode(
path.join(plugin.root, declaration.plugins.manifestFileName),
path.join(pluginDestination, declaration.plugins.manifestFileName),
);
for (const subdirectory of declaration.plugins.subdirectories) {
if (subdirectory.origin !== 'source') {
continue;
}
if (!subdirectoryEnabled(subdirectory, target, features)) {
continue;
}
copyPluginSubdirectory(
declaration,
path.join(plugin.root, subdirectory.path),
path.join(pluginDestination, subdirectory.path),
);
}
}
copyPreparedPayloads({
declaration,
repoRoot,
staging,
target,
features,
});
copyNativePayloads({
declaration,
repoRoot,
srcTauriRoot: destinationRoot,
staging,
target,
features,
profile,
});
});
return {
summary: `plugins 重新生成(${plugins.length} 个插件,写入 ${declaration.plugins.destinationDirectory})`,
record: {
fingerprint: pluginSourceFingerprint(
declaration,
plugins,
target,
features,
),
},
};
}
/// 准备全部随包资源;返回逐条汇总,供入口日志与测试断言。
export function prepareBundledResources({
target = resolveHostTarget(),
destinationRoot = SRC_TAURI_DIR,
declarationPath = DECLARATION_PATH,
features = new Set(defaultEditorFeatures(target)),
recordPath = RECORD_PATH,
lockfilePath = path.join(REPO_ROOT, 'package-lock.json'),
profile = 'debug',
runCommand = defaultRunCommand,
log = () => {},
dryRun = false,
repoRoot = REPO_ROOT,
appRoot = path.dirname(destinationRoot),
} = {}) {
const declaration = readDeclaration(declarationPath);
const record = readRecord(recordPath);
const summaries = [];
let changed = false;
const codex = prepareCodex({
declaration,
target,
destinationRoot,
roots: { app: appRoot, repo: repoRoot },
lockfilePath,
record,
dryRun,
});
summaries.push(codex.summary);
if (codex.record) {
record.codex = {
...record.codex,
[stagingUnit(declaration, target).directory]: codex.record,
};
changed = true;
}
if (
declaration.plugins.targetContainsAny.some((needle) =>
target.includes(needle),
)
) {
ensurePreparedArtifacts({
declaration,
repoRoot,
srcTauriRoot: destinationRoot,
target,
features,
profile,
runCommand,
dryRun,
log,
});
const plugins = preparePlugins({
declaration,
target,
destinationRoot,
repoRoot,
features,
plugins: pluginDirectories(declaration, repoRoot),
record,
profile,
dryRun,
});
summaries.push(plugins.summary);
if (plugins.record) {
record.plugins = plugins.record;
changed = true;
}
} else {
summaries.push(`plugins 跳过(目标 ${target} 不适用)`);
}
const claudeAgent = prepareClaudeAgent({
declaration,
target,
destinationRoot,
roots: { app: appRoot, repo: repoRoot },
record,
dryRun,
});
summaries.push(claudeAgent.summary);
if (claudeAgent.record) {
record.claudeAgent = {
...record.claudeAgent,
[target]: claudeAgent.record,
};
changed = true;
}
if (changed && !dryRun) {
writeRecord(record, recordPath);
}
return summaries;
}
function requireFlagValue(argv, index, flag) {
const value = argv[index + 1];
if (value === undefined || String(value).startsWith('--')) {
fail(
`${flag} 缺少取值(例如 ${flag} <value>);拒绝回退到宿主默认值,以免准备错目标`,
);
}
return String(value);
}
function parseArguments(argv) {
const args = { target: undefined, destinationRoot: undefined, dryRun: false };
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === '--target') {
args.target = requireFlagValue(argv, index, '--target');
index += 1;
} else if (value === '--destination') {
args.destinationRoot = requireFlagValue(argv, index, '--destination');
index += 1;
} else if (value === '--dry-run') {
args.dryRun = true;
} else if (value.startsWith('--features=')) {
args.features = new Set(
value.slice('--features='.length).split(',').filter(Boolean),
);
} else if (value === '--features') {
args.features = new Set(
String(argv[index + 1] ?? '')
.split(',')
.filter(Boolean),
);
index += 1;
} else {
fail(`未知参数:${value}`);
}
}
return args;
}
function main(argv) {
const args = parseArguments(argv);
const summaries = prepareBundledResources({
target: args.target ?? resolveHostTarget(),
destinationRoot: args.destinationRoot ?? SRC_TAURI_DIR,
...(args.features ? { features: args.features } : {}),
dryRun: args.dryRun,
log: (line) => console.log(`[agc-resources] ${line}`),
});
for (const summary of summaries) {
console.log(`[agc-resources] ${summary}`);
}
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
try {
main(process.argv.slice(2));
} catch (error) {
if (error instanceof PrepareError) {
console.error(`[agc-resources] ${error.message}`);
process.exit(1);
}
throw error;
}
}