AGC 编辑器分支产物归位:构建脚本彻底退出写入
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Frontend tests (pull_request) Successful in 1m46s
Project CI / Native shell tests (pull_request) Successful in 6m1s
Project CI / Repository checks (pull_request) Successful in 1m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m11s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Frontend tests (pull_request) Successful in 1m46s
Project CI / Native shell tests (pull_request) Successful in 6m1s
Project CI / Repository checks (pull_request) Successful in 1m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m11s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m53s
- 声明扩展:subdirectories 新增 origin=prepared;libraryStaging 增加 prepare/files;新增 nativePayloads 与 plugins.prepareSteps(程序类型、工作目录、指纹、必需产物) - Godot 随包文件清单改由声明提供,godot_bundle::BUNDLE_FILES 从生成的编译期常量取值,不再各写一份 - 准备步骤按声明运行 powershell.exe -File build.ps1(Unity/Godot,Godot 移除 PSModulePath)与 cargo build -p cocos-editor-bridge --target … --features windows-injection,内容指纹命中且必需产物齐全时零写入,命令执行器可注入以便在 macOS 上覆盖调度与失败关闭 - build.rs 删除 prepare_unity_editor_helper、prepare_godot_editor_extension、stage_cocos_editor_payload、stage_build_generated_plugin_payloads 及辅助函数(415 → 193 行),只留只读校验,并新增已准备产物存在性与 Godot 随包库深度校验;AGC_SKIP_RESOURCE_STAGING 开关随写入分支删除 - 删除两份只含 staging 条目的 .taurignore;发布入口传 profile=release 以定位 Cocos 产物 - 文档:技术方案 §4.9/§8、M3 里程碑(含 Windows 验收清单)、运维文档、决策日志与排障经验同步 - 验证:准备步骤 13 条用例通过(三类准备步骤调度、指纹跳过、缺产物失败关闭、幂等);npm run agc:bundled-resources:check 通过;cargo check --no-default-features 通过且第二次 0.62 秒 fresh - 未验证:Windows 真机(powershell/cargo 路径、产物归位、包内容一致性与客户端加载),按 M3 里程碑验收清单在 Windows 上确认
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -440,6 +440,7 @@ export function stageBundledResources(
|
||||
const summaries = prepare({
|
||||
target,
|
||||
features: new Set(defaultEditorFeatures(target)),
|
||||
profile: 'release',
|
||||
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
|
||||
});
|
||||
for (const summary of summaries) {
|
||||
|
||||
@@ -81,11 +81,11 @@ function expectInteger(value, at) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 随包子目录来源:`source` 由声明与仓库源码就能生成(准备步骤负责),
|
||||
// `build` 由构建期工具链产出(构建脚本在产物生成后负责)。
|
||||
// 随包子目录来源:`source` 直接来自仓库源码(准备步骤复制),
|
||||
// `prepared` 需要先由准备步骤运行声明的构建命令产出,再复制进随包目录。
|
||||
function expectOrigin(value, at) {
|
||||
if (value !== 'source' && value !== 'build') {
|
||||
fail(`${at} 必须是 source 或 build(实际 ${String(value)})`);
|
||||
if (value !== 'source' && value !== 'prepared') {
|
||||
fail(`${at} 必须是 source 或 prepared(实际 ${String(value)})`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -227,15 +227,21 @@ function parseDeclaration(raw) {
|
||||
).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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -445,6 +451,10 @@ 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(
|
||||
declaration.plugins.libraryStaging[0]?.files ?? [],
|
||||
)};
|
||||
|
||||
pub const CODEX: Codex = ${codexStruct};
|
||||
|
||||
pub const PLUGINS: Plugins = ${pluginsStruct};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// 用法(在 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,
|
||||
@@ -700,9 +701,6 @@ function pluginTreeMatches(
|
||||
return false;
|
||||
}
|
||||
for (const subdirectory of declaration.plugins.subdirectories) {
|
||||
if (subdirectory.origin !== 'source') {
|
||||
continue;
|
||||
}
|
||||
if (!subdirectoryEnabled(subdirectory, target, features)) {
|
||||
continue;
|
||||
}
|
||||
@@ -743,13 +741,330 @@ function assertOwnedPluginRoot(destination, plugins) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 声明的准备步骤:需要外部工具链或同一次 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,
|
||||
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,
|
||||
'apps/ai-game-creator-shell/src-tauri/Cargo.toml',
|
||||
),
|
||||
'-p',
|
||||
path.basename(step.packageDirectory),
|
||||
'--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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(';')}`,
|
||||
);
|
||||
}
|
||||
copyFilePreservingMode(
|
||||
source,
|
||||
path.join(
|
||||
repoRoot,
|
||||
'plugins',
|
||||
payload.plugin,
|
||||
payload.destinationSubdirectory,
|
||||
payload.sourceFileName,
|
||||
),
|
||||
);
|
||||
copyFilePreservingMode(
|
||||
source,
|
||||
path.join(
|
||||
staging,
|
||||
payload.plugin,
|
||||
payload.destinationSubdirectory,
|
||||
payload.sourceFileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function preparePlugins({
|
||||
declaration,
|
||||
target,
|
||||
destinationRoot,
|
||||
repoRoot,
|
||||
features,
|
||||
plugins,
|
||||
record,
|
||||
profile,
|
||||
dryRun,
|
||||
}) {
|
||||
const destination = path.join(
|
||||
@@ -799,6 +1114,22 @@ function preparePlugins({
|
||||
);
|
||||
}
|
||||
}
|
||||
copyPreparedPayloads({
|
||||
declaration,
|
||||
repoRoot,
|
||||
staging,
|
||||
target,
|
||||
features,
|
||||
});
|
||||
copyNativePayloads({
|
||||
declaration,
|
||||
repoRoot,
|
||||
srcTauriRoot: destinationRoot,
|
||||
staging,
|
||||
target,
|
||||
features,
|
||||
profile,
|
||||
});
|
||||
});
|
||||
return {
|
||||
summary: `plugins 重新生成(${plugins.length} 个插件,写入 ${declaration.plugins.destinationDirectory})`,
|
||||
@@ -814,6 +1145,9 @@ export function prepareBundledResources({
|
||||
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),
|
||||
@@ -844,13 +1178,25 @@ export function prepareBundledResources({
|
||||
target.includes(needle),
|
||||
)
|
||||
) {
|
||||
ensurePreparedArtifacts({
|
||||
declaration,
|
||||
repoRoot,
|
||||
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);
|
||||
|
||||
@@ -23,6 +23,33 @@ function sha256File(file) {
|
||||
}
|
||||
|
||||
/// 造一个最小工作区:app(含 node_modules 上游包)、repo(含 plugins 工作区)、lockfile。
|
||||
/// 造出声明里所有「已准备」产物:真实构建要 Windows 工具链,用例只需要文件在位。
|
||||
function writePrepareArtifacts(root, declaration) {
|
||||
const created = [];
|
||||
for (const step of declaration.plugins.prepareSteps ?? []) {
|
||||
for (const relative of step.requiredOutputs) {
|
||||
const file = path.join(root, relative);
|
||||
if (fs.existsSync(file)) {
|
||||
continue;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `prepared ${relative}\n`);
|
||||
created.push(relative);
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/// 记录调用的假执行器:默认把声明的必需产物造出来。
|
||||
function fakeRunCommand({ created = [], failOn = null } = {}) {
|
||||
return (invocation) => {
|
||||
created.push(`${invocation.program} ${(invocation.args ?? []).join(' ')}`);
|
||||
if (failOn && invocation.program === failOn) {
|
||||
throw new Error('fake run failure');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-resources-'));
|
||||
const appRoot = path.join(root, 'app');
|
||||
@@ -114,6 +141,18 @@ function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
|
||||
fs.writeFileSync(path.join(pluginRoot, '.env'), 'secret\n');
|
||||
}
|
||||
|
||||
writePrepareArtifacts(repoRoot, declaration);
|
||||
const cocosDll = path.join(
|
||||
destinationRoot,
|
||||
'target',
|
||||
WINDOWS_TARGET,
|
||||
'debug',
|
||||
'deps',
|
||||
'cocos-editor-bridge.dll',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(cocosDll), { recursive: true });
|
||||
fs.writeFileSync(cocosDll, 'cocos bridge payload\n');
|
||||
|
||||
const lockfilePath = path.join(root, 'package-lock.json');
|
||||
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2));
|
||||
return {
|
||||
@@ -156,6 +195,7 @@ function snapshot(directory) {
|
||||
}
|
||||
|
||||
function prepare(fixture, overrides = {}) {
|
||||
const runner = overrides.runCommand ?? fakeRunCommand();
|
||||
return prepareBundledResources({
|
||||
target: WINDOWS_TARGET,
|
||||
destinationRoot: fixture.destinationRoot,
|
||||
@@ -164,6 +204,7 @@ function prepare(fixture, overrides = {}) {
|
||||
lockfilePath: fixture.lockfilePath,
|
||||
repoRoot: fixture.repoRoot,
|
||||
appRoot: fixture.appRoot,
|
||||
runCommand: runner,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
@@ -458,3 +499,88 @@ test('declaration drives source lookup and staging units', () => {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('runs declared prepare steps once and copies their artifacts into the staged tree', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const created = [];
|
||||
const summaries = prepare(fixture, {
|
||||
runCommand: fakeRunCommand({ created }),
|
||||
features: new Set([
|
||||
'unity-editor-execute',
|
||||
'godot-editor-execute',
|
||||
'cocos-editor-injection',
|
||||
]),
|
||||
});
|
||||
assert.match(summaries[0], /命中缓存|重新生成/);
|
||||
assert.ok(
|
||||
created.some(
|
||||
(entry) =>
|
||||
entry.startsWith('powershell.exe') && entry.includes('build.ps1'),
|
||||
),
|
||||
'必须执行声明的 powershell 准备步骤',
|
||||
);
|
||||
assert.ok(
|
||||
created.some(
|
||||
(entry) =>
|
||||
entry.startsWith('cargo build') && entry.includes('--target'),
|
||||
),
|
||||
'必须执行声明的 cargo 准备步骤',
|
||||
);
|
||||
const stagedPayload = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
);
|
||||
assert.ok(fs.existsSync(stagedPayload), 'native payload 必须进随包目录');
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
fixture.repoRoot,
|
||||
'plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
),
|
||||
),
|
||||
'native payload 必须写回插件工作区(唯一真源)',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('skips prepare steps whose fingerprint is unchanged', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const first = [];
|
||||
prepare(fixture, { runCommand: fakeRunCommand({ created: first }) });
|
||||
assert.ok(first.length > 0, '首次必须执行准备步骤');
|
||||
const second = [];
|
||||
prepare(fixture, { runCommand: fakeRunCommand({ created: second }) });
|
||||
const unityRuns = second.filter(
|
||||
(entry) =>
|
||||
entry.startsWith('powershell.exe') &&
|
||||
entry.includes('agc-unity-editor'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
unityRuns,
|
||||
[],
|
||||
'指纹一致时不应再跑声明了指纹的 Unity 准备步骤',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed when a prepare step does not produce its declared outputs', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
fs.rmSync(path.join(fixture.repoRoot, 'plugins/agc-unity-editor'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
assert.throws(
|
||||
() => prepare(fixture, { runCommand: fakeRunCommand() }),
|
||||
/未产出必需文件/,
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -10,7 +10,6 @@ mod godot_bundle;
|
||||
#[path = "build_support/runtime_prompt_bundle.rs"]
|
||||
mod runtime_prompt_bundle;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
@@ -76,9 +75,65 @@ fn validate_staged_resources(manifest_dir: &std::path::Path) {
|
||||
);
|
||||
}
|
||||
validate_staged_plugin_workspace(manifest_dir, &target);
|
||||
validate_prepared_payloads(manifest_dir, &target);
|
||||
}
|
||||
|
||||
/// 只读校验插件随包工作区:声明的源码派生内容必须与仓库源码逐文件一致,整树无符号链接。
|
||||
/// 已准备产物与随包库在本机无法重建,构建期至少要确认它们已经就位。
|
||||
fn validate_prepared_payloads(manifest_dir: &std::path::Path, target: &str) {
|
||||
if !package_layout::plugin_staging_applies(target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
for plugin in package_layout::plugin_directories(
|
||||
&repo_root.join(declared.source_directory),
|
||||
declared.manifest_file_name,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"))
|
||||
{
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_is_prepared(subdirectory)
|
||||
|| !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
let staged = destination_root.join(&plugin.name).join(&relative);
|
||||
if !staged.is_dir() {
|
||||
panic!(
|
||||
"随包已准备产物缺失:{}(请先执行随包资源准备步骤)",
|
||||
staged.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if plugin.name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
let staged = destination_root.join(&plugin.name).join(&relative);
|
||||
godot_bundle::validate(&staged)
|
||||
.unwrap_or_else(|error| panic!("Godot 随包资源校验失败:{error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_staged_plugin_workspace(manifest_dir: &std::path::Path, target: &str) {
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
@@ -104,14 +159,6 @@ fn main() {
|
||||
"cargo:rustc-env=AGC_BUILD_TARGET={}",
|
||||
env::var("TARGET").expect("Cargo TARGET")
|
||||
);
|
||||
// AGC_SKIP_RESOURCE_STAGING=1 只做只读校验(要求随包资源已由准备步骤生成),
|
||||
// 用于在既有产物上单独验证校验路径。
|
||||
if env::var_os("AGC_SKIP_RESOURCE_STAGING").is_none() {
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_build_generated_plugin_payloads(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
}
|
||||
validate_staged_resources(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||
@@ -134,282 +181,3 @@ fn main() {
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
|
||||
return;
|
||||
}
|
||||
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
|
||||
let profile_dir = out_dir
|
||||
.ancestors()
|
||||
.find(|path| path.file_name().is_some_and(|name| name == "build"))
|
||||
.and_then(|build_dir| build_dir.parent())
|
||||
.expect("AGC Cargo profile directory not found");
|
||||
let candidates = [
|
||||
profile_dir.join("deps/cocos_editor_bridge.dll"),
|
||||
profile_dir.join("cocos_editor_bridge.dll"),
|
||||
];
|
||||
let source = candidates
|
||||
.iter()
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Cocos bridge native payload 未构建:{}",
|
||||
candidates
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
)
|
||||
});
|
||||
for destination in [
|
||||
// 插件工作区里的 payload 是开发态与打包态的唯一真源。
|
||||
manifest_dir
|
||||
.join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
// 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
|
||||
manifest_dir
|
||||
.join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
] {
|
||||
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
|
||||
.expect("创建 Cocos bridge payload 目录失败");
|
||||
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||
|
||||
/// Unity helper 是插件的随包运行文件。内容指纹避免每次 Cargo 检查都重新发布 .NET。
|
||||
fn prepare_unity_editor_helper(manifest_dir: &std::path::Path) {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_UNITY_EDITOR_EXECUTE");
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()
|
||||
|| target != "x86_64-pc-windows-msvc"
|
||||
{
|
||||
return;
|
||||
}
|
||||
let root = manifest_dir.join("../../../plugins/agc-unity-editor/dotnet");
|
||||
let mut sources = Vec::new();
|
||||
collect_unity_helper_sources(&root, &mut sources);
|
||||
sources.sort();
|
||||
let mut fingerprint = Sha256::new();
|
||||
for source in &sources {
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
fingerprint.update(
|
||||
source
|
||||
.strip_prefix(&root)
|
||||
.expect("helper source")
|
||||
.to_string_lossy()
|
||||
.as_bytes(),
|
||||
);
|
||||
fingerprint.update([0]);
|
||||
fingerprint.update(fs::read(source).expect("读取 Unity helper 源文件失败"));
|
||||
}
|
||||
let fingerprint = format!("{:x}", fingerprint.finalize());
|
||||
let publish = root.join("publish/win-x64");
|
||||
let executable = publish.join("Agc.Unity.Attach.exe");
|
||||
let stamp = publish.join(".agc-source.sha256");
|
||||
println!("cargo:rerun-if-changed={}", executable.display());
|
||||
if unity_helper_publish_complete(&publish)
|
||||
&& fs::read_to_string(&stamp).ok().as_deref() == Some(&fingerprint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
cfg!(windows),
|
||||
"构建 Unity 插件 helper 需要 Windows .NET 10 与 x64 C++ 工具链"
|
||||
);
|
||||
let status = std::process::Command::new("powershell.exe")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
])
|
||||
.arg(root.join("build.ps1"))
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.expect("无法启动 Unity helper 构建脚本");
|
||||
assert!(
|
||||
status.success() && unity_helper_publish_complete(&publish),
|
||||
"Unity helper 构建失败或缺少运行文件/许可"
|
||||
);
|
||||
fs::write(stamp, fingerprint).expect("写入 Unity helper 构建指纹失败");
|
||||
}
|
||||
|
||||
fn unity_helper_publish_complete(publish: &std::path::Path) -> bool {
|
||||
[
|
||||
"Agc.Unity.Attach.exe",
|
||||
"NOTICE",
|
||||
"THIRD-PARTY-NOTICES.txt",
|
||||
"licenses/DotCraft-Apache-2.0.txt",
|
||||
"licenses/Roslyn-MIT.txt",
|
||||
"licenses/upstream.json",
|
||||
"licenses/dotnet-LICENSE.TXT",
|
||||
"licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||
"licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||
"licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf",
|
||||
]
|
||||
.iter()
|
||||
.all(|name| {
|
||||
fs::symlink_metadata(publish.join(name)).is_ok_and(|metadata| {
|
||||
metadata.is_file() && !metadata.file_type().is_symlink() && metadata.len() > 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBuf>) {
|
||||
for entry in fs::read_dir(root)
|
||||
.expect("Unity helper 源码目录缺失")
|
||||
.flatten()
|
||||
{
|
||||
let kind = entry.file_type().expect("读取 Unity helper 源文件类型失败");
|
||||
assert!(!kind.is_symlink(), "Unity helper 源码不允许符号链接");
|
||||
let name = entry.file_name();
|
||||
if kind.is_dir() {
|
||||
if !matches!(
|
||||
name.to_str(),
|
||||
Some("bin" | "obj" | "publish" | "native-build")
|
||||
) {
|
||||
collect_unity_helper_sources(&entry.path(), sources);
|
||||
}
|
||||
} else if kind.is_file() {
|
||||
sources.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_GODOT_EDITOR_EXECUTE");
|
||||
if env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_none()
|
||||
|| env::var("TARGET").expect("Cargo TARGET") != "x86_64-pc-windows-msvc"
|
||||
{
|
||||
return;
|
||||
}
|
||||
let root = manifest_dir.join("../../../plugins/agc-godot-editor/native/gdextension");
|
||||
for source in godot_bundle::source_files(&root).unwrap_or_else(|error| panic!("{error}")) {
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
assert!(
|
||||
cfg!(windows),
|
||||
"构建 Godot 原生扩展需要 Windows x64 C 编译器"
|
||||
);
|
||||
let status = std::process::Command::new("powershell.exe")
|
||||
// Cargo 可能从 PowerShell 7 启动,Windows PowerShell 应使用自身模块目录。
|
||||
.env_remove("PSModulePath")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
])
|
||||
.arg(root.join("build.ps1"))
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.expect("无法启动 Godot 原生扩展构建脚本");
|
||||
assert!(status.success(), "Godot 原生扩展构建失败");
|
||||
godot_bundle::validate(&root).unwrap_or_else(|error| panic!("{error}"));
|
||||
}
|
||||
|
||||
/// 构建期产物归位:只有构建过程才产出、因而无法由准备步骤生成的随包子目录。
|
||||
///
|
||||
/// 源码派生的子目录由准备步骤在 `tauri dev|build` 之前写入;这里只补构建期才存在的产物。
|
||||
/// 把这批产物也归位到准备步骤(连同编辑器分支产物)在后续里程碑完成。
|
||||
fn stage_build_generated_plugin_payloads(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if !package_layout::plugin_staging_applies(&target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
let plugins = package_layout::plugin_directories(
|
||||
&repo_root.join(declared.source_directory),
|
||||
declared.manifest_file_name,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
for plugin in plugins {
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_is_build_derived(subdirectory)
|
||||
|| !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
let source = plugin.path.join(&relative);
|
||||
if !source.is_dir() {
|
||||
continue;
|
||||
}
|
||||
copy_staged_tree(
|
||||
&source,
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
);
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if plugin.name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
match staging.layout {
|
||||
"godot-bundle" => godot_bundle::stage(
|
||||
&plugin.path.join(&relative),
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
&target,
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}")),
|
||||
other => panic!("未实现的随包库 staging 布局:{other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 复制一棵目录树(按声明跳过构建产物与测试文件);内容一致时不重写。
|
||||
fn copy_staged_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||
if !source.is_dir() {
|
||||
return;
|
||||
}
|
||||
fs::create_dir_all(destination).expect("创建插件资源目录失败");
|
||||
for entry in fs::read_dir(source).expect("读取插件资源失败").flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy().to_string();
|
||||
let target = destination.join(&file_name);
|
||||
assert!(
|
||||
entry
|
||||
.file_type()
|
||||
.expect("读取插件文件类型失败")
|
||||
.is_symlink(),
|
||||
"插件资源不允许符号链接"
|
||||
);
|
||||
if path.is_dir() {
|
||||
if !package_layout::skip_directory(&name) {
|
||||
copy_staged_tree(&path, &target);
|
||||
}
|
||||
} else if !package_layout::skip_file_name(&name) {
|
||||
let bytes = fs::read(&path).expect("读取插件资源失败");
|
||||
if fs::read(&target).is_ok_and(|existing| existing == bytes) {
|
||||
continue;
|
||||
}
|
||||
fs::write(&target, bytes).expect("复制插件资源失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const BUNDLE_FILES: [&str; 4] = [
|
||||
"bin/win-x64/agc_godot_editor.dll",
|
||||
"bin/win-x64/metadata.json",
|
||||
"vendor/LICENSE.txt",
|
||||
"vendor/provenance.json",
|
||||
];
|
||||
// 共享随包资源声明:Godot 随包文件清单与构建期校验共用同一份来源。
|
||||
#[allow(dead_code)]
|
||||
#[path = "package_layout.rs"]
|
||||
mod package_layout;
|
||||
|
||||
pub const BUNDLE_FILES: &[&str] = package_layout::GODOT_BUNDLE_FILES;
|
||||
|
||||
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
|
||||
let metadata = fs::symlink_metadata(path)
|
||||
|
||||
@@ -11,6 +11,8 @@ pub const CODEX_VERSION: &str = "0.155.1";
|
||||
pub const CODEX_CLI_VERSION: &str = "codex-cli 0.155.1";
|
||||
pub const CODEX_MANIFEST_SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||
|
||||
pub const GODOT_BUNDLE_FILES: &[&str] = &["bin/win-x64/agc_godot_editor.dll", "bin/win-x64/metadata.json", "vendor/LICENSE.txt", "vendor/provenance.json"];
|
||||
|
||||
pub const CODEX: Codex = Codex {
|
||||
package_metadata: PackageMetadata {
|
||||
layout_version: 1,
|
||||
@@ -103,7 +105,7 @@ Subdirectory {
|
||||
},
|
||||
Subdirectory {
|
||||
path: "dotnet/publish/win-x64",
|
||||
origin: "build",
|
||||
origin: "prepared",
|
||||
target_contains: &[],
|
||||
targets: &["x86_64-pc-windows-msvc"],
|
||||
features: &["unity-editor-execute"],
|
||||
|
||||
@@ -94,7 +94,8 @@
|
||||
{ "path": "native/payload", "origin": "source", "targetContains": ["windows"] },
|
||||
{
|
||||
"path": "dotnet/publish/win-x64",
|
||||
"origin": "build",
|
||||
"origin": "prepared",
|
||||
"prepare": "unity-helper-publish",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["unity-editor-execute"]
|
||||
}
|
||||
@@ -103,9 +104,71 @@
|
||||
{
|
||||
"plugin": "agc-godot-editor",
|
||||
"sourceSubdirectory": "native/gdextension",
|
||||
"prepare": "godot-extension-build",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["godot-editor-execute"],
|
||||
"layout": "godot-bundle"
|
||||
"layout": "godot-bundle",
|
||||
"files": [
|
||||
"bin/win-x64/agc_godot_editor.dll",
|
||||
"bin/win-x64/metadata.json",
|
||||
"vendor/LICENSE.txt",
|
||||
"vendor/provenance.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"nativePayloads": [
|
||||
{
|
||||
"plugin": "agc-cocos-editor",
|
||||
"prepare": "cocos-bridge-build",
|
||||
"sourceFileName": "cocos-editor-bridge.dll",
|
||||
"destinationSubdirectory": "native/payload",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["cocos-editor-injection"]
|
||||
}
|
||||
],
|
||||
"prepareSteps": [
|
||||
{
|
||||
"name": "unity-helper-publish",
|
||||
"kind": "powershell",
|
||||
"workingDirectory": "plugins/agc-unity-editor/dotnet",
|
||||
"scriptFileName": "build.ps1",
|
||||
"fingerprint": {
|
||||
"roots": ["."],
|
||||
"excludeDirectoryNames": ["bin", "obj", "publish", "native-build"],
|
||||
"stampRelativePath": "publish/win-x64/.agc-source.sha256"
|
||||
},
|
||||
"requiredOutputs": [
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/NOTICE",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/THIRD-PARTY-NOTICES.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/DotCraft-Apache-2.0.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/Roslyn-MIT.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/upstream.json",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-LICENSE.TXT",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "godot-extension-build",
|
||||
"kind": "powershell",
|
||||
"workingDirectory": "plugins/agc-godot-editor/native/gdextension",
|
||||
"scriptFileName": "build.ps1",
|
||||
"removeEnvironment": ["PSModulePath"],
|
||||
"requiredOutputs": [
|
||||
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll",
|
||||
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/metadata.json",
|
||||
"plugins/agc-godot-editor/native/gdextension/vendor/LICENSE.txt",
|
||||
"plugins/agc-godot-editor/native/gdextension/vendor/provenance.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cocos-bridge-build",
|
||||
"kind": "cargo",
|
||||
"packageDirectory": "plugins/agc-cocos-editor/native/cocos-editor-bridge",
|
||||
"features": ["windows-injection"],
|
||||
"requiredOutputs": []
|
||||
}
|
||||
],
|
||||
"skipDirectoryNames": ["target", "node_modules"],
|
||||
|
||||
@@ -286,7 +286,7 @@ pub fn validate_staged_plugins(
|
||||
let relative = declared_relative_path(subdirectory.path);
|
||||
let source = plugin.path.join(&relative);
|
||||
let staged_directory = staged.join(&relative);
|
||||
if subdirectory_is_build_derived(subdirectory) {
|
||||
if subdirectory_is_prepared(subdirectory) {
|
||||
if source.exists() && !staged_directory.is_dir() {
|
||||
return Err(format!(
|
||||
"随包构建期产物缺失:{}(插件 {})",
|
||||
@@ -330,9 +330,9 @@ pub fn subdirectory_is_source_derived(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "source"
|
||||
}
|
||||
|
||||
/// 声明为「由构建期工具链产出」的子目录。
|
||||
pub fn subdirectory_is_build_derived(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "build"
|
||||
/// 声明为「需要先由准备步骤运行构建命令产出」的子目录。
|
||||
pub fn subdirectory_is_prepared(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "prepared"
|
||||
}
|
||||
|
||||
/// 仓库插件目录:含清单文件的普通目录,按名字排序。
|
||||
@@ -853,14 +853,14 @@ mod tests {
|
||||
.filter(|entry| subdirectory_is_source_derived(entry))
|
||||
.map(|entry| entry.path)
|
||||
.collect::<Vec<_>>();
|
||||
let build = PLUGINS
|
||||
let prepared = PLUGINS
|
||||
.subdirectories
|
||||
.iter()
|
||||
.filter(|entry| subdirectory_is_build_derived(entry))
|
||||
.filter(|entry| subdirectory_is_prepared(entry))
|
||||
.map(|entry| entry.path)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(source, ["src", "panels", "skills", "native/payload"]);
|
||||
assert_eq!(build, ["dotnet/publish/win-x64"]);
|
||||
assert_eq!(prepared, ["dotnet/publish/win-x64"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user