547fb0dbce
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m23s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m1s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m51s
Project CI / Repository checks (pull_request) Successful in 1m57s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m39s
- P2:preparePlugins 重排——dry-run 只做只读判断即返回,预缓存交付不再排在 dry-run 之前;所有权断言提到任何写入之前 - P3:pluginSourceFingerprint 放开 origin=source 过滤(prepared 内容哈希分支可达),库文件与 payload 交付态纳入指纹,pluginTreeMatches 增加「不该存在却存在 → 需要重建」判定 - P3:门禁交叉校验 nativePayloads[].destinationSubdirectory 必须是同插件 origin=prepared 的子目录之一 - P2:里程碑验收清单注明 Cocos payload 只在 injection 构建交付并给出对应命令;dry-run 用例补 injection feature 覆盖 - 夹具补齐 agc-cocos-editor 插件(所有权检查要求目标插件真实存在) - 验证:工具用例 13/13、声明门禁通过 - 已知缺口:injection 构建后切回默认 feature,随包目录里陈旧 payload 的清理尚未生效(已记在 PR)
637 lines
21 KiB
JavaScript
Executable File
637 lines
21 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
// 随包资源声明门禁:把 build_support/package-layout.json(唯一人工声明)渲染成
|
||
// build_support/package-layout.generated.rs(Rust 编译期常量),并校验声明结构与不变量。
|
||
//
|
||
// 用法:
|
||
// node scripts/check-package-layout.mjs 校验生成结果是否与声明一致(不一致 exit 1)
|
||
// node scripts/check-package-layout.mjs --write 重新生成
|
||
//
|
||
// 设计约束:Rust 侧不解析 JSON(避免运行期解析与生命周期妥协),只使用本脚本产出的常量;
|
||
// Node 侧准备步骤直接读同一份 JSON。因此本门禁是“单一声明”的机械保障。
|
||
|
||
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||
const APP_ROOT = path.resolve(SCRIPT_DIR, '..');
|
||
const SRC_TAURI = path.join(APP_ROOT, 'src-tauri');
|
||
const DECLARATION_PATH = path.join(
|
||
SRC_TAURI,
|
||
'build_support/package-layout.json',
|
||
);
|
||
const GENERATED_PATH = path.join(
|
||
SRC_TAURI,
|
||
'build_support/package-layout.generated.rs',
|
||
);
|
||
|
||
const EXPECTED_SCHEMA = 'agc-package-layout.v1';
|
||
|
||
class DeclarationError extends Error {}
|
||
|
||
function fail(message) {
|
||
throw new DeclarationError(message);
|
||
}
|
||
|
||
function expectObject(value, at) {
|
||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||
fail(`${at} 必须是对象`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function expectArray(value, at) {
|
||
if (!Array.isArray(value)) {
|
||
fail(`${at} 必须是数组`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function expectString(value, at) {
|
||
if (typeof value !== 'string' || value.length === 0) {
|
||
fail(`${at} 必须是非空字符串`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function expectBoolean(value, at) {
|
||
if (typeof value !== 'boolean') {
|
||
fail(`${at} 必须是布尔值`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function expectStringArray(value, at) {
|
||
return expectArray(value, at).map((item, index) =>
|
||
expectString(item, `${at}[${index}]`),
|
||
);
|
||
}
|
||
|
||
function optionalStringArray(source, key, at) {
|
||
if (source[key] === undefined) {
|
||
return [];
|
||
}
|
||
return expectStringArray(source[key], `${at}.${key}`);
|
||
}
|
||
|
||
function expectInteger(value, at) {
|
||
if (!Number.isInteger(value) || value < 0) {
|
||
fail(`${at} 必须是非负整数`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
// 随包子目录来源:`source` 直接来自仓库源码(准备步骤复制),
|
||
// `prepared` 需要先由准备步骤运行声明的构建命令产出,再复制进随包目录。
|
||
function expectOrigin(value, at) {
|
||
if (value !== 'source' && value !== 'prepared') {
|
||
fail(`${at} 必须是 source 或 prepared(实际 ${String(value)})`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
// JSON 字符串字面量与 Rust 字符串字面量几乎一致,唯一差异是控制字符的 \uXXXX 与 \u{XX}。
|
||
function rustString(value) {
|
||
return JSON.stringify(value).replace(/\\u([0-9a-fA-F]{4})/g, '\\u{$1}');
|
||
}
|
||
|
||
function rustStrings(values) {
|
||
return `&[${values.map(rustString).join(', ')}]`;
|
||
}
|
||
|
||
function indent(level) {
|
||
return ' '.repeat(level);
|
||
}
|
||
|
||
function renderStruct(name, fields, level = 0) {
|
||
const body = fields
|
||
.map(([key, rendered]) => `${indent(level + 1)}${key}: ${rendered},`)
|
||
.join('\n');
|
||
return `${name} {\n${body}\n${indent(level)}}`;
|
||
}
|
||
|
||
function parsePackageMetadata(value) {
|
||
const metadata = expectObject(value, 'codex.packageMetadata');
|
||
return {
|
||
layoutVersion: expectInteger(
|
||
metadata.layoutVersion,
|
||
'codex.packageMetadata.layoutVersion',
|
||
),
|
||
resourcesDir: expectString(
|
||
metadata.resourcesDir,
|
||
'codex.packageMetadata.resourcesDir',
|
||
),
|
||
pathDir: expectString(metadata.pathDir, 'codex.packageMetadata.pathDir'),
|
||
};
|
||
}
|
||
|
||
function parseDeclaration(raw) {
|
||
const root = expectObject(JSON.parse(raw), 'root');
|
||
if (root.schema !== EXPECTED_SCHEMA) {
|
||
fail(
|
||
`不支持的声明 schema:${String(root.schema)}(期望 ${EXPECTED_SCHEMA})`,
|
||
);
|
||
}
|
||
const layoutVersion = expectInteger(root.layoutVersion, 'layoutVersion');
|
||
|
||
const codex = expectObject(root.codex, 'codex');
|
||
const targets = expectArray(codex.targets, 'codex.targets').map(
|
||
(entry, index) => {
|
||
const at = `codex.targets[${index}]`;
|
||
const parsed = expectObject(entry, at);
|
||
const files = expectStringArray(parsed.files, `${at}.files`);
|
||
if (files.length === 0) {
|
||
fail(`${at}.files 不能为空`);
|
||
}
|
||
const executable = expectString(parsed.executable, `${at}.executable`);
|
||
if (!files.includes(executable)) {
|
||
fail(`${at}.executable 必须属于组件白名单:${executable}`);
|
||
}
|
||
return {
|
||
target: expectString(parsed.target, `${at}.target`),
|
||
platform: expectString(parsed.platform, `${at}.platform`),
|
||
directory: expectString(parsed.directory, `${at}.directory`),
|
||
executable,
|
||
files,
|
||
};
|
||
},
|
||
);
|
||
const seenTargets = new Set();
|
||
for (const entry of targets) {
|
||
if (seenTargets.has(entry.target)) {
|
||
fail(`codex.targets 重复声明目标 ${entry.target}`);
|
||
}
|
||
seenTargets.add(entry.target);
|
||
}
|
||
|
||
const noticeSources = expectArray(
|
||
codex.noticeSources,
|
||
'codex.noticeSources',
|
||
).map((entry, index) => {
|
||
const at = `codex.noticeSources[${index}]`;
|
||
const parsed = expectObject(entry, at);
|
||
return {
|
||
targets: expectStringArray(parsed.targets, `${at}.targets`),
|
||
source: expectString(parsed.source, `${at}.source`),
|
||
preserve: expectBoolean(parsed.preserve, `${at}.preserve`),
|
||
};
|
||
});
|
||
for (const entry of targets) {
|
||
const covered = noticeSources.filter((notice) =>
|
||
notice.targets.includes(entry.target),
|
||
);
|
||
if (covered.length !== 1) {
|
||
fail(
|
||
`目标 ${entry.target} 必须且只能有一条第三方声明来源(实际 ${covered.length} 条)`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const universalGroups = expectArray(
|
||
codex.universalGroups,
|
||
'codex.universalGroups',
|
||
).map((entry, index) => {
|
||
const at = `codex.universalGroups[${index}]`;
|
||
const parsed = expectObject(entry, at);
|
||
const groupTargets = expectStringArray(parsed.targets, `${at}.targets`);
|
||
for (const target of groupTargets) {
|
||
if (!seenTargets.has(target)) {
|
||
fail(`${at}.targets 含未声明目标 ${target}`);
|
||
}
|
||
}
|
||
return {
|
||
name: expectString(parsed.name, `${at}.name`),
|
||
directory: expectString(parsed.directory, `${at}.directory`),
|
||
targets: groupTargets,
|
||
};
|
||
});
|
||
|
||
const plugins = expectObject(root.plugins, 'plugins');
|
||
const subdirectories = expectArray(
|
||
plugins.subdirectories,
|
||
'plugins.subdirectories',
|
||
).map((entry, index) => {
|
||
const at = `plugins.subdirectories[${index}]`;
|
||
const parsed = expectObject(entry, at);
|
||
return {
|
||
path: expectString(parsed.path, `${at}.path`),
|
||
origin: expectOrigin(parsed.origin, `${at}.origin`),
|
||
plugin:
|
||
parsed.plugin === undefined
|
||
? ''
|
||
: expectString(parsed.plugin, `${at}.plugin`),
|
||
targetContains: optionalStringArray(parsed, 'targetContains', at),
|
||
targets: optionalStringArray(parsed, 'targets', at),
|
||
features: optionalStringArray(parsed, 'features', at),
|
||
};
|
||
});
|
||
const libraryStaging = expectArray(
|
||
plugins.libraryStaging,
|
||
'plugins.libraryStaging',
|
||
).map((entry, index) => {
|
||
const at = `plugins.libraryStaging[${index}]`;
|
||
const parsed = expectObject(entry, at);
|
||
const files = expectStringArray(parsed.files, `${at}.files`);
|
||
if (files.length === 0) {
|
||
fail(`${at}.files 不能为空`);
|
||
}
|
||
return {
|
||
plugin: expectString(parsed.plugin, `${at}.plugin`),
|
||
sourceSubdirectory: expectString(
|
||
parsed.sourceSubdirectory,
|
||
`${at}.sourceSubdirectory`,
|
||
),
|
||
prepare: expectString(parsed.prepare, `${at}.prepare`),
|
||
targets: expectStringArray(parsed.targets, `${at}.targets`),
|
||
features: expectStringArray(parsed.features, `${at}.features`),
|
||
layout: expectString(parsed.layout, `${at}.layout`),
|
||
files,
|
||
};
|
||
});
|
||
|
||
return {
|
||
layoutVersion,
|
||
codex: {
|
||
version: expectString(codex.version, 'codex.version'),
|
||
cliVersionPrefix: expectString(
|
||
codex.cliVersionPrefix,
|
||
'codex.cliVersionPrefix',
|
||
),
|
||
manifestSchema: expectString(
|
||
codex.manifestSchema,
|
||
'codex.manifestSchema',
|
||
),
|
||
packageMetadata: parsePackageMetadata(codex.packageMetadata),
|
||
resourceDirectory: expectString(
|
||
codex.resourceDirectory,
|
||
'codex.resourceDirectory',
|
||
),
|
||
manifestFileName: expectString(
|
||
codex.manifestFileName,
|
||
'codex.manifestFileName',
|
||
),
|
||
packageMetadataFileName: expectString(
|
||
codex.packageMetadataFileName,
|
||
'codex.packageMetadataFileName',
|
||
),
|
||
noticeFileName: expectString(
|
||
codex.noticeFileName,
|
||
'codex.noticeFileName',
|
||
),
|
||
sourceRoots: expectStringArray(codex.sourceRoots, 'codex.sourceRoots'),
|
||
sourceRelativePaths: expectStringArray(
|
||
codex.sourceRelativePaths,
|
||
'codex.sourceRelativePaths',
|
||
),
|
||
noticeSources,
|
||
universalGroups,
|
||
targets,
|
||
},
|
||
plugins: {
|
||
sourceDirectory: expectString(
|
||
plugins.sourceDirectory,
|
||
'plugins.sourceDirectory',
|
||
),
|
||
destinationDirectory: expectString(
|
||
plugins.destinationDirectory,
|
||
'plugins.destinationDirectory',
|
||
),
|
||
manifestFileName: expectString(
|
||
plugins.manifestFileName,
|
||
'plugins.manifestFileName',
|
||
),
|
||
targetContainsAny: expectStringArray(
|
||
plugins.targetContainsAny,
|
||
'plugins.targetContainsAny',
|
||
),
|
||
subdirectories,
|
||
libraryStaging,
|
||
skipDirectoryNames: expectStringArray(
|
||
plugins.skipDirectoryNames,
|
||
'plugins.skipDirectoryNames',
|
||
),
|
||
skipDirectoryNamePrefixes: expectStringArray(
|
||
plugins.skipDirectoryNamePrefixes,
|
||
'plugins.skipDirectoryNamePrefixes',
|
||
),
|
||
skipFileNamePrefixes: expectStringArray(
|
||
plugins.skipFileNamePrefixes,
|
||
'plugins.skipFileNamePrefixes',
|
||
),
|
||
skipFileNameFragments: expectStringArray(
|
||
plugins.skipFileNameFragments,
|
||
'plugins.skipFileNameFragments',
|
||
),
|
||
},
|
||
};
|
||
}
|
||
|
||
function renderCodexTarget(entry) {
|
||
return renderStruct(
|
||
'CodexTarget',
|
||
[
|
||
['target', rustString(entry.target)],
|
||
['platform', rustString(entry.platform)],
|
||
['directory', rustString(entry.directory)],
|
||
['executable', rustString(entry.executable)],
|
||
['files', rustStrings(entry.files)],
|
||
],
|
||
1,
|
||
);
|
||
}
|
||
|
||
function renderNoticeSource(entry) {
|
||
return renderStruct(
|
||
'NoticeSource',
|
||
[
|
||
['targets', rustStrings(entry.targets)],
|
||
['source', rustString(entry.source)],
|
||
['preserve', entry.preserve ? 'true' : 'false'],
|
||
],
|
||
1,
|
||
);
|
||
}
|
||
|
||
function renderUniversalGroup(entry) {
|
||
return renderStruct(
|
||
'UniversalGroup',
|
||
[
|
||
['name', rustString(entry.name)],
|
||
['directory', rustString(entry.directory)],
|
||
['targets', rustStrings(entry.targets)],
|
||
],
|
||
1,
|
||
);
|
||
}
|
||
|
||
function renderSubdirectory(entry) {
|
||
return renderStruct(
|
||
'Subdirectory',
|
||
[
|
||
['path', rustString(entry.path)],
|
||
['plugin', rustString(entry.plugin ?? '')],
|
||
['origin', rustString(entry.origin)],
|
||
['target_contains', rustStrings(entry.targetContains)],
|
||
['targets', rustStrings(entry.targets)],
|
||
['features', rustStrings(entry.features)],
|
||
],
|
||
1,
|
||
);
|
||
}
|
||
|
||
function renderLibraryStaging(entry) {
|
||
return renderStruct(
|
||
'LibraryStaging',
|
||
[
|
||
['plugin', rustString(entry.plugin)],
|
||
['source_subdirectory', rustString(entry.sourceSubdirectory)],
|
||
['targets', rustStrings(entry.targets)],
|
||
['features', rustStrings(entry.features)],
|
||
['layout', rustString(entry.layout)],
|
||
],
|
||
1,
|
||
);
|
||
}
|
||
|
||
function renderGenerated(declaration) {
|
||
const codex = declaration.codex;
|
||
const godotStaging = declaration.plugins.libraryStaging.find(
|
||
(entry) => entry.layout === 'godot-bundle',
|
||
);
|
||
if (!godotStaging || godotStaging.files.length === 0) {
|
||
fail('声明缺少 godot-bundle 随包文件清单,拒绝生成空清单');
|
||
}
|
||
const godotFiles = godotStaging.files;
|
||
const plugins = declaration.plugins;
|
||
const codexStruct = renderStruct('Codex', [
|
||
[
|
||
'package_metadata',
|
||
renderStruct('PackageMetadata', [
|
||
['layout_version', String(codex.packageMetadata.layoutVersion)],
|
||
['resources_dir', rustString(codex.packageMetadata.resourcesDir)],
|
||
['path_dir', rustString(codex.packageMetadata.pathDir)],
|
||
]),
|
||
],
|
||
['resource_directory', rustString(codex.resourceDirectory)],
|
||
['manifest_file_name', rustString(codex.manifestFileName)],
|
||
['package_metadata_file_name', rustString(codex.packageMetadataFileName)],
|
||
['notice_file_name', rustString(codex.noticeFileName)],
|
||
['source_roots', rustStrings(codex.sourceRoots)],
|
||
['source_relative_paths', rustStrings(codex.sourceRelativePaths)],
|
||
[
|
||
'notice_sources',
|
||
`&[\n${codex.noticeSources.map(renderNoticeSource).join(',\n')}\n]`,
|
||
],
|
||
[
|
||
'universal_groups',
|
||
`&[\n${codex.universalGroups.map(renderUniversalGroup).join(',\n')}\n]`,
|
||
],
|
||
['targets', `&[\n${codex.targets.map(renderCodexTarget).join(',\n')}\n]`],
|
||
]);
|
||
const pluginsStruct = renderStruct('Plugins', [
|
||
['source_directory', rustString(plugins.sourceDirectory)],
|
||
['destination_directory', rustString(plugins.destinationDirectory)],
|
||
['manifest_file_name', rustString(plugins.manifestFileName)],
|
||
['target_contains_any', rustStrings(plugins.targetContainsAny)],
|
||
[
|
||
'subdirectories',
|
||
`&[\n${plugins.subdirectories.map(renderSubdirectory).join(',\n')}\n]`,
|
||
],
|
||
[
|
||
'library_staging',
|
||
`&[\n${plugins.libraryStaging.map(renderLibraryStaging).join(',\n')}\n]`,
|
||
],
|
||
['skip_directory_names', rustStrings(plugins.skipDirectoryNames)],
|
||
[
|
||
'skip_directory_name_prefixes',
|
||
rustStrings(plugins.skipDirectoryNamePrefixes),
|
||
],
|
||
['skip_file_name_prefixes', rustStrings(plugins.skipFileNamePrefixes)],
|
||
['skip_file_name_fragments', rustStrings(plugins.skipFileNameFragments)],
|
||
]);
|
||
|
||
return `// @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs
|
||
// 来源:build_support/package-layout.json。不要手工编辑本文件。
|
||
// 修改随包资源布局请编辑声明文件,然后运行
|
||
// npm run agc:package-layout:sync(在仓库根目录)
|
||
// 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。
|
||
|
||
pub const DECLARATION_SCHEMA: &str = ${rustString(EXPECTED_SCHEMA)};
|
||
pub const LAYOUT_VERSION: u64 = ${declaration.layoutVersion};
|
||
|
||
pub const CODEX_VERSION: &str = ${rustString(declaration.codex.version)};
|
||
pub const CODEX_CLI_VERSION: &str = ${rustString(declaration.codex.cliVersionPrefix + declaration.codex.version)};
|
||
pub const CODEX_MANIFEST_SCHEMA: &str = ${rustString(declaration.codex.manifestSchema)};
|
||
|
||
pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings(godotFiles)};
|
||
|
||
pub const CODEX: Codex = ${codexStruct};
|
||
|
||
pub const PLUGINS: Plugins = ${pluginsStruct};
|
||
`;
|
||
}
|
||
|
||
function appLockedCodexVersion() {
|
||
const appPackage = expectObject(
|
||
JSON.parse(readFileSync(path.join(APP_ROOT, 'package.json'), 'utf8')),
|
||
'apps/ai-game-creator-shell/package.json',
|
||
);
|
||
const declared =
|
||
appPackage.dependencies?.['@openai/codex'] ??
|
||
appPackage.devDependencies?.['@openai/codex'];
|
||
if (typeof declared !== 'string' || declared.length === 0) {
|
||
fail(
|
||
'应用 package.json 未声明 @openai/codex,无法校验声明的 codex.version',
|
||
);
|
||
}
|
||
return declared.replace(/^[\^~]/, '');
|
||
}
|
||
|
||
/// 新 section(prepareSteps / nativePayloads / prepared 来源)不参与 Rust 生成物,
|
||
/// 必须在门禁里单独把关,避免「声明了却没人用」或引用到不存在的准备步骤。
|
||
function validateExtendedDeclarations() {
|
||
const root = expectObject(
|
||
JSON.parse(readFileSync(DECLARATION_PATH, 'utf8')),
|
||
'root',
|
||
);
|
||
const plugins = expectObject(root.plugins, 'plugins');
|
||
const steps = expectArray(plugins.prepareSteps ?? [], 'plugins.prepareSteps');
|
||
const names = new Set();
|
||
for (const [index, raw] of steps.entries()) {
|
||
const at = `plugins.prepareSteps[${index}]`;
|
||
const step = expectObject(raw, at);
|
||
const name = expectString(step.name, `${at}.name`);
|
||
if (names.has(name)) {
|
||
fail(`${at}.name 重复:${name}`);
|
||
}
|
||
names.add(name);
|
||
const kind = expectString(step.kind, `${at}.kind`);
|
||
if (kind !== 'powershell' && kind !== 'cargo') {
|
||
fail(`${at}.kind 只能是 powershell 或 cargo`);
|
||
}
|
||
if (kind === 'powershell') {
|
||
expectString(step.workingDirectory, `${at}.workingDirectory`);
|
||
expectString(step.scriptFileName, `${at}.scriptFileName`);
|
||
} else {
|
||
expectString(step.packageDirectory, `${at}.packageDirectory`);
|
||
}
|
||
expectStringArray(step.requiredOutputs ?? [], `${at}.requiredOutputs`);
|
||
}
|
||
const payloads = expectArray(
|
||
plugins.nativePayloads ?? [],
|
||
'plugins.nativePayloads',
|
||
);
|
||
for (const [index, raw] of payloads.entries()) {
|
||
const at = `plugins.nativePayloads[${index}]`;
|
||
const payload = expectObject(raw, at);
|
||
expectString(payload.plugin, `${at}.plugin`);
|
||
expectString(payload.prepare, `${at}.prepare`);
|
||
expectString(payload.sourceFileName, `${at}.sourceFileName`);
|
||
expectString(payload.destinationFileName, `${at}.destinationFileName`);
|
||
expectString(
|
||
payload.destinationSubdirectory,
|
||
`${at}.destinationSubdirectory`,
|
||
);
|
||
if (!names.has(payload.prepare)) {
|
||
fail(`${at}.prepare 引用了未声明的准备步骤:${payload.prepare}`);
|
||
}
|
||
}
|
||
const preparedByPlugin = new Map();
|
||
for (const entry of expectArray(
|
||
plugins.subdirectories ?? [],
|
||
'plugins.subdirectories',
|
||
)) {
|
||
const subdirectory = expectObject(entry, 'subdirectory');
|
||
if (subdirectory.origin !== 'prepared') {
|
||
continue;
|
||
}
|
||
const owner = expectString(
|
||
subdirectory.plugin ?? '',
|
||
'subdirectory.plugin',
|
||
);
|
||
if (!owner) {
|
||
fail(`origin=prepared 的子目录必须声明 plugin:${subdirectory.path}`);
|
||
}
|
||
preparedByPlugin.set(owner, [
|
||
...(preparedByPlugin.get(owner) ?? []),
|
||
subdirectory.path,
|
||
]);
|
||
}
|
||
for (const payload of payloads) {
|
||
const candidates = preparedByPlugin.get(payload.plugin) ?? [];
|
||
if (!candidates.includes(payload.destinationSubdirectory)) {
|
||
fail(
|
||
`nativePayloads 的 destinationSubdirectory(${payload.destinationSubdirectory})必须是该插件 origin=prepared 的子目录之一(现有:${candidates.join('、') || '无'})`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const referenced = [
|
||
...expectArray(plugins.subdirectories ?? [], 'plugins.subdirectories')
|
||
.filter(
|
||
(entry) => expectObject(entry, 'subdirectory').origin === 'prepared',
|
||
)
|
||
.map((entry) => [entry.path, entry.prepare]),
|
||
...expectArray(plugins.libraryStaging ?? [], 'plugins.libraryStaging').map(
|
||
(entry) => [entry.sourceSubdirectory, entry.prepare],
|
||
),
|
||
...payloads.map((entry) => [
|
||
`${entry.plugin}/${entry.destinationSubdirectory}`,
|
||
entry.prepare,
|
||
]),
|
||
];
|
||
for (const [label, prepare] of referenced) {
|
||
if (!prepare) {
|
||
fail(`prepared 来源的条目缺少 prepare 声明:${label}`);
|
||
}
|
||
if (!names.has(prepare)) {
|
||
fail(`${label} 引用了未声明的准备步骤:${prepare}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function main() {
|
||
const declaration = parseDeclaration(readFileSync(DECLARATION_PATH, 'utf8'));
|
||
validateExtendedDeclarations();
|
||
const lockedVersion = appLockedCodexVersion();
|
||
if (lockedVersion !== declaration.codex.version) {
|
||
fail(
|
||
`声明 codex.version(${declaration.codex.version})与应用锁定的 @openai/codex(${lockedVersion})不一致;请同步更新 build_support/package-layout.json`,
|
||
);
|
||
}
|
||
const rendered = renderGenerated(declaration);
|
||
const write = process.argv.includes('--write');
|
||
if (write) {
|
||
writeFileSync(GENERATED_PATH, rendered);
|
||
console.log(
|
||
`随包资源声明已生成:${path.relative(process.cwd(), GENERATED_PATH)}(layoutVersion ${declaration.layoutVersion})`,
|
||
);
|
||
return;
|
||
}
|
||
const current = readFileSync(GENERATED_PATH, 'utf8');
|
||
if (current !== rendered) {
|
||
console.error(
|
||
[
|
||
`随包资源声明与 Rust 常量不一致:`,
|
||
` 声明:${path.relative(process.cwd(), DECLARATION_PATH)}`,
|
||
` 生成:${path.relative(process.cwd(), GENERATED_PATH)}`,
|
||
'请运行:npm run agc:package-layout:sync',
|
||
].join('\n'),
|
||
);
|
||
process.exit(1);
|
||
}
|
||
console.log(
|
||
`随包资源声明一致(layoutVersion ${declaration.layoutVersion},codex ${declaration.codex.version},目标 ${declaration.codex.targets.length},插件子目录 ${declaration.plugins.subdirectories.length})`,
|
||
);
|
||
}
|
||
|
||
try {
|
||
main();
|
||
} catch (error) {
|
||
if (error instanceof DeclarationError) {
|
||
console.error(`随包资源声明无效:${error.message}`);
|
||
process.exit(1);
|
||
}
|
||
throw error;
|
||
}
|