Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92c05428ec | |||
| 91f1554ac7 | |||
| 6a0b75779b |
@@ -13,7 +13,7 @@ import {
|
|||||||
defaultEditorFeatures,
|
defaultEditorFeatures,
|
||||||
withDefaultCargoFeatures,
|
withDefaultCargoFeatures,
|
||||||
} from './cargo-features.mjs';
|
} from './cargo-features.mjs';
|
||||||
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
import { stageNodeRuntimeForTarget } from './stage-node-runtime.mjs';
|
||||||
|
|
||||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||||
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
|
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
|
||||||
@@ -459,7 +459,8 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
|
|||||||
export function runTauriBuild(
|
export function runTauriBuild(
|
||||||
args = [],
|
args = [],
|
||||||
context = resolveReleaseContext(args),
|
context = resolveReleaseContext(args),
|
||||||
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
|
// 默认按发布目标 stage:universal 需要两份架构运行时,单架构目标行为不变。
|
||||||
|
{ spawn = spawnSync, stageRuntime = stageNodeRuntimeForTarget } = {},
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
explicitBuildTarget(args) &&
|
explicitBuildTarget(args) &&
|
||||||
|
|||||||
@@ -76,6 +76,33 @@ function run(command, args) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单独执行随包 Node 分片:分片架构与宿主一致时直接跑,不一致时用 `arch` 强制
|
||||||
|
* (arm64 机器上的 x86_64 分片依赖 Rosetta,与 `.app` 双架构 smoke 的前提相同)。
|
||||||
|
*/
|
||||||
|
function runNodeSlice(directory, args) {
|
||||||
|
const native = process.arch === 'arm64' ? 'arm64' : 'x86_64';
|
||||||
|
const binary = path.join(directory, 'node');
|
||||||
|
const [command, argv] =
|
||||||
|
architecture === native
|
||||||
|
? [binary, args]
|
||||||
|
: ['/usr/bin/arch', [`-${architecture}`, binary, ...args]];
|
||||||
|
const result = spawnSync(command, argv, {
|
||||||
|
cwd: root,
|
||||||
|
env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 120_000,
|
||||||
|
maxBuffer: 1024 * 1024,
|
||||||
|
});
|
||||||
|
assert.ifError(result.error);
|
||||||
|
assert.equal(
|
||||||
|
result.status,
|
||||||
|
0,
|
||||||
|
`${directory} 随包 Node 执行失败:${result.stderr || result.stdout}`,
|
||||||
|
);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APFS 上优先用 `ditto --clone`:整包按区块克隆,秒级完成且几乎不占额外空间。
|
* APFS 上优先用 `ditto --clone`:整包按区块克隆,秒级完成且几乎不占额外空间。
|
||||||
* 跨卷或非 APFS 时回退到真实复制;两种路径都必须产出可独立改动的副本,
|
* 跨卷或非 APFS 时回退到真实复制;两种路径都必须产出可独立改动的副本,
|
||||||
@@ -248,37 +275,84 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
|
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
|
||||||
|
// 随包 Node:单架构构建是扁平目录,universal 构建把两套架构运行时并列放进
|
||||||
|
// `game-runtime/node/<platform>-<arch>/`(与 Codex 侧车同形态,由 Rust 侧按运行架构选择)。
|
||||||
|
// 两套清单在本函数里都做完整性与架构校验;只执行与本次 smoke 架构一致的那一份,
|
||||||
|
// 另一份由另一次架构的 smoke 覆盖(build-macos-ci.mjs 会对 arm64 / x86_64 各跑一次)。
|
||||||
const nodeRoot = path.join(resources, 'game-runtime/node');
|
const nodeRoot = path.join(resources, 'game-runtime/node');
|
||||||
const nodeManifest = JSON.parse(
|
const nodeSlices = requireUniversal
|
||||||
fs.readFileSync(path.join(nodeRoot, 'manifest.json'), 'utf8'),
|
? ['darwin-arm64', 'darwin-x64'].map((platform) => ({
|
||||||
);
|
platform,
|
||||||
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
|
directory: path.join(nodeRoot, platform),
|
||||||
assert.equal(nodeManifest.platform, 'darwin');
|
}))
|
||||||
assert.equal(nodeManifest.arch, process.arch);
|
: [{ platform: null, directory: nodeRoot }];
|
||||||
const runtimeFiles = fs
|
for (const slice of nodeSlices) {
|
||||||
.readdirSync(nodeRoot, { recursive: true })
|
const { directory } = slice;
|
||||||
.filter(
|
const nodeManifest = JSON.parse(
|
||||||
(file) =>
|
fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'),
|
||||||
fs.statSync(path.join(nodeRoot, file)).isFile() &&
|
|
||||||
file !== 'manifest.json',
|
|
||||||
);
|
);
|
||||||
assert.deepEqual(runtimeFiles.sort(), Object.keys(nodeManifest.files).sort());
|
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
|
||||||
for (const [file, digest] of Object.entries(nodeManifest.files)) {
|
assert.equal(nodeManifest.platform, 'darwin');
|
||||||
assert.equal(await hashFile(path.join(nodeRoot, file)), digest, file);
|
if (slice.platform) {
|
||||||
|
// 目录名与清单架构必须一致:错位会让用户拿到跑不起来的运行时。
|
||||||
|
assert.equal(`darwin-${nodeManifest.arch}`, slice.platform);
|
||||||
|
assert.ok(
|
||||||
|
/^(arm64|x64)$/u.test(nodeManifest.arch),
|
||||||
|
`未知运行架构:${nodeManifest.arch}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert.equal(nodeManifest.arch, process.arch);
|
||||||
|
}
|
||||||
|
const runtimeFiles = fs
|
||||||
|
.readdirSync(directory, { recursive: true })
|
||||||
|
.filter(
|
||||||
|
(file) =>
|
||||||
|
fs.statSync(path.join(directory, file)).isFile() &&
|
||||||
|
file !== 'manifest.json',
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
runtimeFiles.sort(),
|
||||||
|
Object.keys(nodeManifest.files).sort(),
|
||||||
|
slice.platform ?? 'flat',
|
||||||
|
);
|
||||||
|
for (const [file, digest] of Object.entries(nodeManifest.files)) {
|
||||||
|
assert.equal(await hashFile(path.join(directory, file)), digest, file);
|
||||||
|
}
|
||||||
|
assert.ok(nodeManifest.files['NODE-LICENSE']);
|
||||||
|
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
|
||||||
|
fs.accessSync(path.join(directory, 'node'), fs.constants.X_OK);
|
||||||
|
// 二进制本身必须是本分片的单一架构:官方发行版不做 universal,lipo 能直接证明。
|
||||||
|
const sliceArchitecture = spawnSync(
|
||||||
|
'/usr/bin/lipo',
|
||||||
|
['-archs', path.join(directory, 'node')],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
assert.equal(sliceArchitecture.status, 0, 'lipo -archs node');
|
||||||
|
assert.equal(
|
||||||
|
sliceArchitecture.stdout.trim(),
|
||||||
|
nodeManifest.arch === 'x64' ? 'x86_64' : 'arm64',
|
||||||
|
'随包 Node 的二进制架构必须等于清单架构',
|
||||||
|
);
|
||||||
|
// 只有与本次 smoke 架构一致的分片才执行;另一架构留给对应的那次 smoke。
|
||||||
|
if (
|
||||||
|
!requireUniversal ||
|
||||||
|
nodeManifest.arch === (architecture === 'arm64' ? 'arm64' : 'x64')
|
||||||
|
) {
|
||||||
|
assert.equal(
|
||||||
|
runNodeSlice(directory, ['--version']),
|
||||||
|
nodeManifest.nodeVersion,
|
||||||
|
`${slice.platform ?? 'flat'} node --version`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
runNodeSlice(directory, [
|
||||||
|
path.join(directory, 'node_modules/npm/bin/npm-cli.js'),
|
||||||
|
'--version',
|
||||||
|
]),
|
||||||
|
nodeManifest.npmVersion,
|
||||||
|
`${slice.platform ?? 'flat'} npm --version`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.ok(nodeManifest.files['NODE-LICENSE']);
|
|
||||||
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
|
|
||||||
assert.equal(
|
|
||||||
run(path.join(nodeRoot, 'node'), ['--version']).stdout.trim(),
|
|
||||||
nodeManifest.nodeVersion,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
run(path.join(nodeRoot, 'node'), [
|
|
||||||
path.join(nodeRoot, 'node_modules/npm/bin/npm-cli.js'),
|
|
||||||
'--version',
|
|
||||||
]).stdout.trim(),
|
|
||||||
nodeManifest.npmVersion,
|
|
||||||
);
|
|
||||||
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
|
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
|
||||||
for (const file of [
|
for (const file of [
|
||||||
'plugin.json',
|
'plugin.json',
|
||||||
@@ -294,8 +368,13 @@ try {
|
|||||||
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/.test(
|
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/.test(
|
||||||
file,
|
file,
|
||||||
) ||
|
) ||
|
||||||
|
// 只有随包 Node 自带的 npm 允许出现 node_modules;两种布局都要放行:
|
||||||
|
// 单架构的 `game-runtime/node/node_modules/npm` 与 universal 的
|
||||||
|
// `game-runtime/node/<platform>-<arch>/node_modules/npm`。
|
||||||
(/(^|\/)node_modules(\/|$)/.test(file) &&
|
(/(^|\/)node_modules(\/|$)/.test(file) &&
|
||||||
!file.startsWith('game-runtime/node/node_modules/npm')),
|
!/^game-runtime\/node\/((darwin-(arm64|x64))\/)?node_modules\/npm(\/|$)/u.test(
|
||||||
|
file,
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
|
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||||
export const nodeRuntimeSchema = 'agc-node-runtime.v1';
|
export const nodeRuntimeSchema = 'agc-node-runtime.v1';
|
||||||
|
// 单架构目标写扁平目录;universal 在此目录下按架构分目录,见 stageNodeRuntimeForTarget。
|
||||||
|
const defaultRuntimeDestination = path.join(
|
||||||
|
appRoot,
|
||||||
|
'src-tauri',
|
||||||
|
'resources',
|
||||||
|
'node-runtime',
|
||||||
|
);
|
||||||
|
|
||||||
export function readInstalledNodeLicense(
|
export function readInstalledNodeLicense(
|
||||||
version,
|
version,
|
||||||
@@ -66,10 +73,118 @@ export function targetRuntime(target) {
|
|||||||
'x86_64-apple-darwin': ['darwin', 'x64'],
|
'x86_64-apple-darwin': ['darwin', 'x64'],
|
||||||
};
|
};
|
||||||
const value = targets[target];
|
const value = targets[target];
|
||||||
if (!value) throw new Error(`Node 运行时不支持发布目标:${target}`);
|
// universal 不是单份运行时能表达的目标:它必须按架构展开成两份,见 targetRuntimes。
|
||||||
|
if (!value)
|
||||||
|
throw new Error(
|
||||||
|
target === 'universal-apple-darwin'
|
||||||
|
? 'Node 运行时不能直接按 universal-apple-darwin 制作:双架构请用 stageNodeRuntimeForTarget 按架构展开'
|
||||||
|
: `Node 运行时不支持发布目标:${target}`,
|
||||||
|
);
|
||||||
return { platform: value[0], arch: value[1] };
|
return { platform: value[0], arch: value[1] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布目标需要的随包运行时列表:universal 需要两份单架构运行时,
|
||||||
|
* 其余目标仍然是一份(保持既有扁平布局与行为)。
|
||||||
|
*/
|
||||||
|
export function targetRuntimes(target) {
|
||||||
|
if (target === 'universal-apple-darwin')
|
||||||
|
return ['aarch64-apple-darwin', 'x86_64-apple-darwin'];
|
||||||
|
return [target];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按架构选择 staging 输入。
|
||||||
|
*
|
||||||
|
* 契约(见实施计划):发布包只从**本机已安装且与目标平台/架构一致**的工具链取材,
|
||||||
|
* 不得使用项目内或相对 PATH 的伪造运行时。宿主架构直接复用当前 Node;
|
||||||
|
* 其它架构必须由构建节点显式提供,缺失即失败关闭——不静默跳过、不回退系统 Node。
|
||||||
|
*/
|
||||||
|
export function runtimeSourceForTarget(
|
||||||
|
archTarget,
|
||||||
|
{
|
||||||
|
env = process.env,
|
||||||
|
nodePath,
|
||||||
|
npmCli,
|
||||||
|
licensePath,
|
||||||
|
hostNodePath = process.execPath,
|
||||||
|
hostNpmCli = process.env.npm_execpath,
|
||||||
|
hostLicensePath = env.AGC_NODE_LICENSE_PATH,
|
||||||
|
hostPlatform = process.platform,
|
||||||
|
hostArch = process.arch,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const native = targetRuntime(archTarget);
|
||||||
|
if (native.platform === hostPlatform && native.arch === hostArch) {
|
||||||
|
return {
|
||||||
|
nodePath: nodePath ?? hostNodePath,
|
||||||
|
npmCli: npmCli ?? hostNpmCli,
|
||||||
|
licensePath: licensePath ?? hostLicensePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const key = `${native.platform}_${native.arch}`.toUpperCase();
|
||||||
|
const configured = env[`AGC_NODE_RUNTIME_${key}_PATH`]?.trim();
|
||||||
|
if (!configured) {
|
||||||
|
throw new Error(
|
||||||
|
`缺少 ${native.platform}/${native.arch} 的 Node 运行时:该架构不是构建宿主,` +
|
||||||
|
`请先在本机安装同架构 Node,再用 AGC_NODE_RUNTIME_${key}_PATH 指向其 bin/node`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nodePath: configured,
|
||||||
|
// 明令不使用宿主 npm(`npm_execpath`):另一架构的 npm 必须来自它自己那份发行版
|
||||||
|
// 安装目录,否则两份切片的 npm 来源不同源,架构与来源对不上。null 表示"按 node 目录查找"。
|
||||||
|
npmCli: null,
|
||||||
|
licensePath: env[`AGC_NODE_RUNTIME_${key}_LICENSE_PATH`]?.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按发布目标 stage 运行时资源。
|
||||||
|
*
|
||||||
|
* 单架构目标沿用既有扁平目录(`node-runtime/`),universal 目标写进
|
||||||
|
* `node-runtime/<platform>-<arch>/`——与捆绑 Codex 的分架构目录同一形态,
|
||||||
|
* 由 Rust 侧按当前运行架构选择;构建期资源映射仍是整目录映射,无需按架构分叉。
|
||||||
|
*
|
||||||
|
* 先解析并校验**全部**来源,再逐个落盘:非宿主架构的运行时缺失、平台/架构不符、
|
||||||
|
* 两套版本不一致,都在写出任何运行时目录之前失败,不留下半套资源冒充发布内容。
|
||||||
|
*/
|
||||||
|
export function stageNodeRuntimeForTarget(
|
||||||
|
target,
|
||||||
|
{ destination = defaultRuntimeDestination, ...options } = {},
|
||||||
|
) {
|
||||||
|
const targets = targetRuntimes(target);
|
||||||
|
const plans = targets.map((archTarget) => {
|
||||||
|
const native = targetRuntime(archTarget);
|
||||||
|
return {
|
||||||
|
destination:
|
||||||
|
targets.length === 1
|
||||||
|
? destination
|
||||||
|
: path.join(destination, `${native.platform}-${native.arch}`),
|
||||||
|
inspected: inspectRuntimeSource(archTarget, {
|
||||||
|
...options,
|
||||||
|
...runtimeSourceForTarget(archTarget, options),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// 两个切片必须是同一套 Node:版本不一致意味着其中一份被换过,
|
||||||
|
// 用户在不同架构上会拿到行为不同的工具链。
|
||||||
|
const identities = new Set(
|
||||||
|
plans.map(
|
||||||
|
({ inspected }) =>
|
||||||
|
`${inspected.info.version}|${inspected.npmPackage.version}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (identities.size !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`多架构 Node 运行时版本不一致:${[...identities].join(' / ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return plans.map(({ inspected, destination }) =>
|
||||||
|
writeRuntimeBundle(inspected, { destination }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function inside(root, file) {
|
function inside(root, file) {
|
||||||
const relative = path.relative(root, file);
|
const relative = path.relative(root, file);
|
||||||
return (
|
return (
|
||||||
@@ -212,13 +327,19 @@ export function assertPortableMacNode(output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stageNodeRuntime(
|
/**
|
||||||
|
* 只读校验一份运行时来源,返回 staging 需要的全部事实。
|
||||||
|
*
|
||||||
|
* 与写盘分离的原因:多架构发布必须能在写出任何文件之前发现「另一份来源缺失或
|
||||||
|
* 与目标不符」。校验口径保持原样——平台/架构必须等于目标、macOS 二进制只能链接
|
||||||
|
* 系统动态库、npm 的身份与实际版本必须一致、许可必须来自发行版本体。
|
||||||
|
*/
|
||||||
|
export function inspectRuntimeSource(
|
||||||
target,
|
target,
|
||||||
{
|
{
|
||||||
nodePath = process.execPath,
|
nodePath = process.execPath,
|
||||||
npmCli = process.env.npm_execpath,
|
npmCli = process.env.npm_execpath,
|
||||||
licensePath = process.env.AGC_NODE_LICENSE_PATH,
|
licensePath = process.env.AGC_NODE_LICENSE_PATH,
|
||||||
destination = path.join(appRoot, 'src-tauri', 'resources', 'node-runtime'),
|
|
||||||
execute = execFileSync,
|
execute = execFileSync,
|
||||||
installedLicense = readInstalledNodeLicense,
|
installedLicense = readInstalledNodeLicense,
|
||||||
} = {},
|
} = {},
|
||||||
@@ -253,6 +374,7 @@ export function stageNodeRuntime(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const nodeDirectory = path.dirname(node);
|
const nodeDirectory = path.dirname(node);
|
||||||
|
// `npmCli: null` 表示显式拒绝沿用宿主 npm,只按这份 node 自己的安装目录查找。
|
||||||
const npmCandidates = [
|
const npmCandidates = [
|
||||||
npmCli,
|
npmCli,
|
||||||
path.join(nodeDirectory, 'node_modules/npm/bin/npm-cli.js'),
|
path.join(nodeDirectory, 'node_modules/npm/bin/npm-cli.js'),
|
||||||
@@ -309,6 +431,14 @@ export function stageNodeRuntime(
|
|||||||
throw new Error('Node LICENSE 不包含发行许可');
|
throw new Error('Node LICENSE 不包含发行许可');
|
||||||
if (!fs.statSync(path.join(npmRoot, 'LICENSE')).isFile())
|
if (!fs.statSync(path.join(npmRoot, 'LICENSE')).isFile())
|
||||||
throw new Error('npm 缺少 LICENSE');
|
throw new Error('npm 缺少 LICENSE');
|
||||||
|
return { native, node, npmRoot, npmPackage, info, license, licenseName };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把已校验的来源写成一份发布资源,返回清单。 */
|
||||||
|
function writeRuntimeBundle(
|
||||||
|
{ native, node, npmRoot, npmPackage, info, license, licenseName },
|
||||||
|
{ destination },
|
||||||
|
) {
|
||||||
// 临时同级目录完成后才替换资源;不污染 Node 安装或项目工作区。
|
// 临时同级目录完成后才替换资源;不污染 Node 安装或项目工作区。
|
||||||
const requestedDestination = path.resolve(destination);
|
const requestedDestination = path.resolve(destination);
|
||||||
if (requestedDestination === path.dirname(requestedDestination))
|
if (requestedDestination === path.dirname(requestedDestination))
|
||||||
@@ -394,3 +524,26 @@ export function stageNodeRuntime(
|
|||||||
cleanupStaging(staging, parent, stagingPrefix, stagingIdentity);
|
cleanupStaging(staging, parent, stagingPrefix, stagingIdentity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stageNodeRuntime(
|
||||||
|
target,
|
||||||
|
{
|
||||||
|
nodePath = process.execPath,
|
||||||
|
npmCli = process.env.npm_execpath,
|
||||||
|
licensePath = process.env.AGC_NODE_LICENSE_PATH,
|
||||||
|
destination = defaultRuntimeDestination,
|
||||||
|
execute = execFileSync,
|
||||||
|
installedLicense = readInstalledNodeLicense,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
return writeRuntimeBundle(
|
||||||
|
inspectRuntimeSource(target, {
|
||||||
|
nodePath,
|
||||||
|
npmCli,
|
||||||
|
licensePath,
|
||||||
|
execute,
|
||||||
|
installedLicense,
|
||||||
|
}),
|
||||||
|
{ destination },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
assertPortableMacNode,
|
assertPortableMacNode,
|
||||||
readInstalledNodeLicense,
|
readInstalledNodeLicense,
|
||||||
stageNodeRuntime,
|
stageNodeRuntime,
|
||||||
|
stageNodeRuntimeForTarget,
|
||||||
targetRuntime,
|
targetRuntime,
|
||||||
|
targetRuntimes,
|
||||||
} from './stage-node-runtime.mjs';
|
} from './stage-node-runtime.mjs';
|
||||||
|
|
||||||
function fixture(run) {
|
function fixture(run) {
|
||||||
@@ -281,7 +283,11 @@ test('native target and macOS dynamic dependency policy reject nonportable Node'
|
|||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
arch: 'arm64',
|
arch: 'arm64',
|
||||||
});
|
});
|
||||||
assert.throws(() => targetRuntime('universal-apple-darwin'), /不支持/u);
|
// universal 不是单份运行时目标:必须报出「按架构展开」而不是笼统的「不支持」。
|
||||||
|
assert.throws(
|
||||||
|
() => targetRuntime('universal-apple-darwin'),
|
||||||
|
/stageNodeRuntimeForTarget/u,
|
||||||
|
);
|
||||||
assertPortableMacNode(
|
assertPortableMacNode(
|
||||||
'/node:\n\t/usr/lib/libSystem.B.dylib (compatibility version 1)\n',
|
'/node:\n\t/usr/lib/libSystem.B.dylib (compatibility version 1)\n',
|
||||||
);
|
);
|
||||||
@@ -293,3 +299,164 @@ test('native target and macOS dynamic dependency policy reject nonportable Node'
|
|||||||
/非系统动态库/u,
|
/非系统动态库/u,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 双架构夹具:每个架构一份来源目录,execute 按被查询的二进制回报对应架构。
|
||||||
|
function universalFixture(run) {
|
||||||
|
const root = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'agc-node-universal-test-'),
|
||||||
|
);
|
||||||
|
const sourceFor = (arch, nodeVersion) => {
|
||||||
|
const dir = path.join(root, `source-${arch}`);
|
||||||
|
const npm = path.join(dir, 'node_modules/npm');
|
||||||
|
fs.mkdirSync(path.join(npm, 'bin'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'node'), `node-${arch}`);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'LICENSE'),
|
||||||
|
'Node.js\nPermission is hereby granted',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(npm, 'LICENSE'), 'npm distribution license');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(npm, 'package.json'),
|
||||||
|
JSON.stringify({ name: 'npm', version: '11.0.0' }),
|
||||||
|
);
|
||||||
|
for (const name of ['npm', 'npx'])
|
||||||
|
fs.writeFileSync(path.join(npm, `bin/${name}-cli.js`), `// ${arch}`);
|
||||||
|
return {
|
||||||
|
nodePath: path.join(dir, 'node'),
|
||||||
|
npmCli: path.join(npm, 'bin/npm-cli.js'),
|
||||||
|
licensePath: path.join(dir, 'LICENSE'),
|
||||||
|
nodeVersion,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return run(root, {
|
||||||
|
arm64: sourceFor('arm64', 'v22.23.2'),
|
||||||
|
x64: sourceFor('x64', 'v22.23.2'),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionsFor(root, sources, overrides = {}) {
|
||||||
|
const archOf = (file) => (file.includes('source-x64') ? 'x64' : 'arm64');
|
||||||
|
return {
|
||||||
|
destination: path.join(root, 'resources/node-runtime'),
|
||||||
|
hostPlatform: 'darwin',
|
||||||
|
hostArch: 'arm64',
|
||||||
|
hostNodePath: sources.arm64.nodePath,
|
||||||
|
hostNpmCli: sources.arm64.npmCli,
|
||||||
|
env: {
|
||||||
|
AGC_NODE_RUNTIME_DARWIN_X64_PATH: sources.x64.nodePath,
|
||||||
|
AGC_NODE_RUNTIME_DARWIN_X64_LICENSE_PATH: sources.x64.licensePath,
|
||||||
|
},
|
||||||
|
installedLicense() {
|
||||||
|
throw new Error('no installed fixture license');
|
||||||
|
},
|
||||||
|
execute(file, args) {
|
||||||
|
if (file === '/usr/bin/otool')
|
||||||
|
return `\t/usr/lib/libSystem.B.dylib\n\t/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\n`;
|
||||||
|
if (args[0] === '-p') {
|
||||||
|
const arch = archOf(file);
|
||||||
|
return JSON.stringify({
|
||||||
|
platform: 'darwin',
|
||||||
|
arch,
|
||||||
|
version: sources[arch].nodeVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '11.0.0';
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('universal stages one runtime per architecture with architecture-correct manifests', () =>
|
||||||
|
universalFixture((root, sources) => {
|
||||||
|
assert.deepEqual(targetRuntimes('universal-apple-darwin'), [
|
||||||
|
'aarch64-apple-darwin',
|
||||||
|
'x86_64-apple-darwin',
|
||||||
|
]);
|
||||||
|
// 模拟经 `npm run` 触发的构建:npm 把宿主的 npm-cli.js 放进环境变量。
|
||||||
|
// 另一架构的切片必须无视它,只能使用自己发行版目录里的 npm。
|
||||||
|
const previousNpmExecPath = process.env.npm_execpath;
|
||||||
|
process.env.npm_execpath = sources.arm64.npmCli;
|
||||||
|
let manifests;
|
||||||
|
try {
|
||||||
|
manifests = stageNodeRuntimeForTarget(
|
||||||
|
'universal-apple-darwin',
|
||||||
|
optionsFor(root, sources),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (previousNpmExecPath === undefined) delete process.env.npm_execpath;
|
||||||
|
else process.env.npm_execpath = previousNpmExecPath;
|
||||||
|
}
|
||||||
|
assert.deepEqual(
|
||||||
|
manifests.map((manifest) => `${manifest.platform}-${manifest.arch}`),
|
||||||
|
['darwin-arm64', 'darwin-x64'],
|
||||||
|
);
|
||||||
|
for (const manifest of manifests) {
|
||||||
|
const directory = path.join(
|
||||||
|
root,
|
||||||
|
'resources/node-runtime',
|
||||||
|
`${manifest.platform}-${manifest.arch}`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
JSON.parse(
|
||||||
|
fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'),
|
||||||
|
).arch,
|
||||||
|
manifest.arch,
|
||||||
|
);
|
||||||
|
assert.ok(fs.existsSync(path.join(directory, 'node')));
|
||||||
|
assert.ok(
|
||||||
|
fs.existsSync(path.join(directory, 'node_modules/npm/LICENSE')),
|
||||||
|
);
|
||||||
|
// 每份切片必须自带对应架构发行版的 npm,不能借用宿主那一份。
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(directory, 'node_modules/npm/bin/npm-cli.js'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
`// ${manifest.arch}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 单架构目标仍写扁平目录(与既有发布一致),不产生分架构子目录。
|
||||||
|
const flat = stageNodeRuntimeForTarget(
|
||||||
|
'aarch64-apple-darwin',
|
||||||
|
optionsFor(root, sources, { destination: path.join(root, 'flat') }),
|
||||||
|
);
|
||||||
|
assert.equal(flat.length, 1);
|
||||||
|
assert.ok(fs.existsSync(path.join(root, 'flat/manifest.json')));
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'flat/darwin-arm64')));
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('universal fails closed when the non-host architecture runtime is absent', () =>
|
||||||
|
universalFixture((root, sources) => {
|
||||||
|
const options = optionsFor(root, sources, { env: {} });
|
||||||
|
assert.throws(
|
||||||
|
() => stageNodeRuntimeForTarget('universal-apple-darwin', options),
|
||||||
|
/AGC_NODE_RUNTIME_DARWIN_X64_PATH/u,
|
||||||
|
);
|
||||||
|
// 缺失时不得留下半成品目录。
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'resources')));
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('universal rejects mismatched Node versions between the two architectures', () =>
|
||||||
|
universalFixture((root, sources) => {
|
||||||
|
const options = optionsFor(root, sources);
|
||||||
|
options.execute = (file, args) => {
|
||||||
|
if (file === '/usr/bin/otool') return '\t/usr/lib/libSystem.B.dylib\n';
|
||||||
|
if (args[0] === '-p')
|
||||||
|
return JSON.stringify({
|
||||||
|
platform: 'darwin',
|
||||||
|
arch: file.includes('source-x64') ? 'x64' : 'arm64',
|
||||||
|
version: file.includes('source-x64') ? 'v22.23.2' : 'v24.0.0',
|
||||||
|
});
|
||||||
|
return '11.0.0';
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => stageNodeRuntimeForTarget('universal-apple-darwin', options),
|
||||||
|
/版本不一致/u,
|
||||||
|
);
|
||||||
|
// 版本核对在写出任何架构之前完成,失败时不留下半套运行时。
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'resources')));
|
||||||
|
}));
|
||||||
|
|||||||
@@ -290,15 +290,37 @@ fn resolve_at(
|
|||||||
let root = root
|
let root = root
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| "project-root-unavailable")?;
|
.map_err(|_| "project-root-unavailable")?;
|
||||||
if let Some(bundle) = bundle.filter(|bundle| bundle.exists()) {
|
// 候选顺序:既有扁平布局(单架构构建),其后是按**当前运行架构**命名的子目录
|
||||||
validate_bundle(bundle)?;
|
// (universal 构建把两套运行时并列放进去)。两个候选都要过 manifest 的平台/架构
|
||||||
return runtime_from_paths(
|
// 校验,因此顺序不会让另一架构的运行时被采用;存在但校验失败则失败关闭,
|
||||||
&root,
|
// 绝不因此退回系统 Node。
|
||||||
bundle.join(executable_name()),
|
let mut bundle_error: Option<String> = None;
|
||||||
bundle.join("node_modules/npm/bin/npm-cli.js"),
|
if let Some(base) = bundle {
|
||||||
"bundled",
|
let candidates = [
|
||||||
path,
|
base.to_path_buf(),
|
||||||
);
|
base.join(format!("{}-{}", native_platform(), native_arch())),
|
||||||
|
];
|
||||||
|
for candidate in candidates.iter().filter(|candidate| candidate.exists()) {
|
||||||
|
match validate_bundle(candidate) {
|
||||||
|
Ok(()) => {
|
||||||
|
return runtime_from_paths(
|
||||||
|
&root,
|
||||||
|
candidate.join(executable_name()),
|
||||||
|
candidate.join("node_modules/npm/bin/npm-cli.js"),
|
||||||
|
"bundled",
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if bundle_error.is_none() {
|
||||||
|
bundle_error = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(error) = bundle_error {
|
||||||
|
return Err(error);
|
||||||
}
|
}
|
||||||
if !development {
|
if !development {
|
||||||
return Err("node-runtime-bundle-missing".into());
|
return Err("node-runtime-bundle-missing".into());
|
||||||
@@ -595,6 +617,52 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// universal 构建把两套架构运行时并列放在 `<base>/<platform>-<arch>/`:
|
||||||
|
/// 解析必须按当前运行架构选中自己那一份,且另一架构的存在与否不影响结果。
|
||||||
|
#[test]
|
||||||
|
fn universal_bundle_resolves_the_directory_matching_the_running_architecture() {
|
||||||
|
let project = tempfile::tempdir().unwrap();
|
||||||
|
let bundle = tempfile::tempdir().unwrap();
|
||||||
|
let current = bundle
|
||||||
|
.path()
|
||||||
|
.join(format!("{}-{}", native_platform(), native_arch()));
|
||||||
|
fs::create_dir_all(¤t).unwrap();
|
||||||
|
bundle_fixture(¤t);
|
||||||
|
|
||||||
|
let runtime = resolve_at(project.path(), Some(bundle.path()), false, OsStr::new(""))
|
||||||
|
.expect("universal bundle must resolve the running architecture");
|
||||||
|
assert_eq!(runtime.source, "bundled");
|
||||||
|
assert_eq!(
|
||||||
|
runtime.node.canonicalize().unwrap(),
|
||||||
|
current.join(executable_name()).canonicalize().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 另一架构的目录即使损坏也不能影响本架构选择。
|
||||||
|
let other = if native_arch() == "arm64" {
|
||||||
|
"x86_64"
|
||||||
|
} else {
|
||||||
|
"arm64"
|
||||||
|
};
|
||||||
|
let other_dir = bundle
|
||||||
|
.path()
|
||||||
|
.join(format!("{}-{}", native_platform(), other));
|
||||||
|
fs::create_dir_all(&other_dir).unwrap();
|
||||||
|
fs::write(other_dir.join("manifest.json"), b"not-json").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resolve_at(project.path(), Some(bundle.path()), false, OsStr::new(""))
|
||||||
|
.unwrap()
|
||||||
|
.node
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap(),
|
||||||
|
current.join(executable_name()).canonicalize().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 本架构目录缺失而另一架构完整时,绝不采用另一架构的运行时。
|
||||||
|
fs::remove_dir_all(¤t).unwrap();
|
||||||
|
bundle_fixture(&other_dir);
|
||||||
|
assert!(resolve_at(project.path(), Some(bundle.path()), false, OsStr::new("")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn development_runtime_rejects_relative_and_project_path_entries() {
|
fn development_runtime_rejects_relative_and_project_path_entries() {
|
||||||
let root = tempfile::tempdir().unwrap();
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
- 对象布局:清单固定写成 `agc/<channel>-win|mac/latest.json`;安装包与签名写成同一分区的 `<version>/<file>` 与 `<file>.sig`。
|
- 对象布局:清单固定写成 `agc/<channel>-win|mac/latest.json`;安装包与签名写成同一分区的 `<version>/<file>` 与 `<file>.sig`。
|
||||||
- macOS 正式交付使用 universal 主程序:两个平台键指向同一个 `.app.tar.gz` 与签名,一份产物同时服务 Apple Silicon 与 Intel。单架构目标(`aarch64-apple-darwin` / `x86_64-apple-darwin`)只用于本机诊断,不登记正式分区清单——单架构构建不可轮流覆盖同一个 `latest.json` 并宣称双架构均可更新。
|
- macOS 正式交付使用 universal 主程序:两个平台键指向同一个 `.app.tar.gz` 与签名,一份产物同时服务 Apple Silicon 与 Intel。单架构目标(`aarch64-apple-darwin` / `x86_64-apple-darwin`)只用于本机诊断,不登记正式分区清单——单架构构建不可轮流覆盖同一个 `latest.json` 并宣称双架构均可更新。
|
||||||
- universal 主程序同时携带分目录的 arm64/x64 原生 Codex 组件:每个组件保持上游单架构布局与独立 SHA-256 清单,运行中的主程序切片只选择同架构目录,不得把两套原生包的元数据或辅助程序混装。
|
- universal 主程序同时携带分目录的 arm64/x64 原生 Codex 组件:每个组件保持上游单架构布局与独立 SHA-256 清单,运行中的主程序切片只选择同架构目录,不得把两套原生包的元数据或辅助程序混装。随包 Node 运行时同样按架构分目录(`game-runtime/node/darwin-arm64/`、`darwin-x64/`,各带 `agc-node-runtime.v1` 清单):两套都来自节点上同版本的官方发行版,构建时缺一份或版本不一致即在写出资源前失败,运行切片只采用与当前架构匹配的那一份,单架构构建保持扁平目录不变。
|
||||||
- 构建期要求:打开 `bundle.createUpdaterArtifacts` 以生成 `.sig`;构建环境提供签名私钥与密码(私钥内容不得入库);公钥写入客户端配置。公钥在首个带更新能力的版本发布后不可更换,更换等于放弃自动更新(只能手动重装)。
|
- 构建期要求:打开 `bundle.createUpdaterArtifacts` 以生成 `.sig`;构建环境提供签名私钥与密码(私钥内容不得入库);公钥写入客户端配置。公钥在首个带更新能力的版本发布后不可更换,更换等于放弃自动更新(只能手动重装)。
|
||||||
- 版本递增按渠道及系统分区独立进行:发布脚本读取该分区远端 `latest.json` 的 `version`,与本地版本取较高者递增 patch;不同分区的远端版本互不影响。
|
- 版本递增按渠道及系统分区独立进行:发布脚本读取该分区远端 `latest.json` 的 `version`,与本地版本取较高者递增 patch;不同分区的远端版本互不影响。
|
||||||
- 版本高水位:仅 dev 的 Windows 分区在迁移窗口内取「分区清单版本」与「旧协议迁移指针版本」较大值再递增,避免已发布旧客户端版本倒退。迁移窗口结束(旧指针 404)后只读分区清单;release、自定义渠道与所有 Mac 分区均不参与旧指针比较。
|
- 版本高水位:仅 dev 的 Windows 分区在迁移窗口内取「分区清单版本」与「旧协议迁移指针版本」较大值再递增,避免已发布旧客户端版本倒退。迁移窗口结束(旧指针 404)后只读分区清单;release、自定义渠道与所有 Mac 分区均不参与旧指针比较。
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
### 环境与工作流
|
### 环境与工作流
|
||||||
|
|
||||||
- 客户端交付配套 Node/npm;发布包从本机已安装且与目标平台/架构一致的工具链制作受校验资源,保留许可并校验内容摘要。安装态不依赖系统 PATH 的 Node;开发态可使用已验证的宿主运行时。不得从项目或相对 PATH 加载伪造运行时。
|
- 客户端交付配套 Node/npm;发布包从本机已安装且与目标平台/架构一致的工具链制作受校验资源,保留许可并校验内容摘要。安装态不依赖系统 PATH 的 Node;开发态可使用已验证的宿主运行时。不得从项目或相对 PATH 加载伪造运行时。universal 包携带**两套**架构运行时并按架构并列存放(`game-runtime/node/<platform>-<arch>/`,各自一份清单),运行切片只采用与当前架构一致的那一份;两套必须来自同一 Node 版本,缺一份或版本不一致时构建在写出任何资源之前失败,不能用「少带一份」或退回系统 Node 充数。
|
||||||
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
|
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
|
||||||
- 预检不安装依赖、不修改项目 revision、不请求平台生成;构建仍执行项目自己的 npm 脚本。Codex 隔离 HOME 与平台凭据边界保持不变,客户端把已验证的运行时加入执行 PATH,不能把宿主凭据目录交给模型。
|
- 预检不安装依赖、不修改项目 revision、不请求平台生成;构建仍执行项目自己的 npm 脚本。Codex 隔离 HOME 与平台凭据边界保持不变,客户端把已验证的运行时加入执行 PATH,不能把宿主凭据目录交给模型。
|
||||||
- 第一轮先明确本次必需玩法、素材和验收项。同批独立读取尽量合并,必需图片一次规划;已有且可用的资产复用。已有目标全部通过后给出交付结果,非阻塞的新点子列为后续工作,不在收尾时主动开启新的生产链。
|
- 第一轮先明确本次必需玩法、素材和验收项。同批独立读取尽量合并,必需图片一次规划;已有且可用的资产复用。已有目标全部通过后给出交付结果,非阻塞的新点子列为后续工作,不在收尾时主动开启新的生产链。
|
||||||
|
|||||||
@@ -736,6 +736,23 @@ Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenki
|
|||||||
|
|
||||||
发布凭据全部走 Jenkins 全局凭据,并在 `withCredentials` 内注入当前进程:`AgcUpdaterSigningKey`(与 `AgcUpdaterSigningKeyPassword`)映射为 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,`AliyunAccessKeyId` / `AliyunaccessKeySecret` 映射为 `AGC_OSS_ACCESS_KEY_ID` / `AGC_OSS_ACCESS_KEY_SECRET`;私钥与凭据不写入 workspace、日志或归档产物。上传顺序为更新包、签名、首装包,三者全部成功后才覆盖 `agc/<channel>-mac/latest.json` 指针;`AGC_RELEASE_DRY_RUN` 与 Windows 渠道对称:默认关闭即真发布,勾选后才退化为演练(只打印将上传的对象、不写任何 OSS 对象)。本 Job 是正式发布入口,调度器在发号后与 Windows 一起触发它,并额外传 `SKIP_IF_SUPERSEDED=true`——Mac 节点是日常办公机,离线期间排队的旧构建在节点回来后若已被源码分支推进,直接跳过而不发布过期版本。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。
|
发布凭据全部走 Jenkins 全局凭据,并在 `withCredentials` 内注入当前进程:`AgcUpdaterSigningKey`(与 `AgcUpdaterSigningKeyPassword`)映射为 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,`AliyunAccessKeyId` / `AliyunaccessKeySecret` 映射为 `AGC_OSS_ACCESS_KEY_ID` / `AGC_OSS_ACCESS_KEY_SECRET`;私钥与凭据不写入 workspace、日志或归档产物。上传顺序为更新包、签名、首装包,三者全部成功后才覆盖 `agc/<channel>-mac/latest.json` 指针;`AGC_RELEASE_DRY_RUN` 与 Windows 渠道对称:默认关闭即真发布,勾选后才退化为演练(只打印将上传的对象、不写任何 OSS 对象)。本 Job 是正式发布入口,调度器在发号后与 Windows 一起触发它,并额外传 `SKIP_IF_SUPERSEDED=true`——Mac 节点是日常办公机,离线期间排队的旧构建在节点回来后若已被源码分支推进,直接跳过而不发布过期版本。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。
|
||||||
|
|
||||||
|
随包 Node 运行时也是节点前置,且 universal 需要**两套架构各一份**:客户端发布包内嵌 Node + npm 作为受校验资源(`agc-node-runtime.v1` 清单 + 逐文件 SHA-256),构建时只从**本机已安装、且与目标平台/架构一致**的工具链取材,缺文件、版本不符或目标不匹配立即失败,禁止回退系统 Node、也不允许从项目或相对 PATH 里借用运行时。宿主架构那一份直接取构建 Node(Mac 节点为 arm64,即 PATH 上的 `node`);另一架构必须由节点显式提供:预装官方 darwin-x64 发行版到 `~/Library/Jenkins/node/darwin-x64`(位置可用 `AGC_NODE_RUNTIME_DARWIN_X64_HOME` 覆盖),保留 `LICENSE` 与 `lib/node_modules/npm`,Job 会把它们导出为 `AGC_NODE_RUNTIME_DARWIN_X64_PATH` / `AGC_NODE_RUNTIME_DARWIN_X64_LICENSE_PATH`。两套必须**同版本**(首次接入时均为 `v22.23.2`):缺一份、架构不符或版本不一致,staging 都会在写出任何运行时目录之前失败关闭,不会生成半套资源的更新包;版本核对同时是 Job 的编译前预检。预装与核对命令(官方 dist,校验 `SHASUMS256.txt`,再确认架构与版本):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
version=22.23.2
|
||||||
|
curl -fsSLO "https://nodejs.org/dist/v${version}/node-v${version}-darwin-x64.tar.gz"
|
||||||
|
curl -fsSLO "https://nodejs.org/dist/v${version}/SHASUMS256.txt"
|
||||||
|
grep " node-v${version}-darwin-x64.tar.gz\$" SHASUMS256.txt | shasum -a 256 -c -
|
||||||
|
mkdir -p "$HOME/Library/Jenkins/node/darwin-x64"
|
||||||
|
tar -xzf "node-v${version}-darwin-x64.tar.gz" -C "$HOME/Library/Jenkins/node/darwin-x64" --strip-components=1
|
||||||
|
/usr/bin/lipo -archs "$HOME/Library/Jenkins/node/darwin-x64/bin/node" # 期望 x86_64
|
||||||
|
/usr/bin/arch -x86_64 "$HOME/Library/Jenkins/node/darwin-x64/bin/node" --version # 期望与构建 Node 相同
|
||||||
|
```
|
||||||
|
|
||||||
|
universal 包的随包 Node 按架构并列存放:`Contents/Resources/game-runtime/node/darwin-arm64/` 与 `darwin-x64/`,各自带独立 `manifest.json`;Rust 侧按当前运行架构选择对应目录(两个候选都要过清单的平台/架构校验,存在但损坏则失败关闭,绝不退回系统 Node),架构校验脚本对两份都做完整性核对与 `lipo -archs` 架构核对,只执行与本次 smoke 架构一致的那一份,另一份由另一次架构的 smoke 覆盖。单架构目标仍写扁平目录 `game-runtime/node/`,行为不变。体积代价:官方 Node + npm 解包后约 130 MB/份(实测 arm64 `node` 112.9 MB + npm 16.3 MB,x64 `node` 115.4 MB + npm 15.6 MB),universal 两份合计约 260 MB,DMG 压缩后增量预计在 50 MB 量级——这是双架构必须付的代价,不通过「Mac 跳过随包 Node」来省。
|
||||||
|
|
||||||
|
同一条规则适用于本机跨架构诊断构建:宿主架构以外的目标(例如 Apple Silicon 上打 `x86_64-apple-darwin`)必须用 `AGC_NODE_RUNTIME_<PLATFORM>_<ARCH>_PATH` 指定来源,缺失时报「缺少 <platform>/<arch> 的 Node 运行时」并给出该变量名,不再退化成含义模糊的「平台/架构不一致」。
|
||||||
|
|
||||||
产物边界:macOS 代码签名与公证暂缺,构建通过剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名,**不得使用 `--no-sign`**——该标志会连带跳过 updater 的 minisign 签名,产物缺少 `.sig` 会直接卡在验签门禁(首次实跑即命中该坑)。构建清单按 `codesign -dv` 实测记录 `appleSigned` / `appleSignatureKind`(如 `adhoc`),并固定记录 `notarized=false`。用户首次安装需要在 Gatekeeper 中手动放行;更新包校验本身只依赖 minisign 签名,因此未签名不阻断自动更新的校验环节,但「安装 → 重启接管新版本」的实机闭环仍未验证,不得以构建成功替代。
|
产物边界:macOS 代码签名与公证暂缺,构建通过剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名,**不得使用 `--no-sign`**——该标志会连带跳过 updater 的 minisign 签名,产物缺少 `.sig` 会直接卡在验签门禁(首次实跑即命中该坑)。构建清单按 `codesign -dv` 实测记录 `appleSigned` / `appleSignatureKind`(如 `adhoc`),并固定记录 `notarized=false`。用户首次安装需要在 Gatekeeper 中手动放行;更新包校验本身只依赖 minisign 签名,因此未签名不阻断自动更新的校验环节,但「安装 → 重启接管新版本」的实机闭环仍未验证,不得以构建成功替代。
|
||||||
|
|
||||||
Job 描述与节点描述是 Jenkins 侧元数据,**不随仓库同步**:Jenkinsfile 只回写参数定义,描述必须手工维护,否则会停留在建 Job 时的口径(2026-09-20 就出现过描述还写着「只构建归档、不签名不上传」,而实际已经是含签名、分区清单、验签与 OSS 上传的发布管线)。当前口径:Job 描述说明「构建 universal → 双架构 smoke → DMG → `<channel>-mac` 分区清单 → 更新包验签 → 按 dry-run 决定上传,未做 Apple 签名与公证」;节点描述说明「`genarrative-agc-macos`、EXCLUSIVE 单 executor、仅手动触发、独立 workspace、sccache 在 HOME」。维护手法:该 Job 上 `POST job/<name>/config.xml` 会返回 500,节点侧同接口正常;因此改 Job 描述用脚本接口原地更新(保留构建历史),不要为了改描述删 Job 重建。
|
Job 描述与节点描述是 Jenkins 侧元数据,**不随仓库同步**:Jenkinsfile 只回写参数定义,描述必须手工维护,否则会停留在建 Job 时的口径(2026-09-20 就出现过描述还写着「只构建归档、不签名不上传」,而实际已经是含签名、分区清单、验签与 OSS 上传的发布管线)。当前口径:Job 描述说明「构建 universal → 双架构 smoke → DMG → `<channel>-mac` 分区清单 → 更新包验签 → 按 dry-run 决定上传,未做 Apple 签名与公证」;节点描述说明「`genarrative-agc-macos`、EXCLUSIVE 单 executor、仅手动触发、独立 workspace、sccache 在 HOME」。维护手法:该 Job 上 `POST job/<name>/config.xml` 会返回 500,节点侧同接口正常;因此改 Job 描述用脚本接口原地更新(保留构建历史),不要为了改描述删 Job 重建。
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ pipeline {
|
|||||||
CARGO_INCREMENTAL = '0'
|
CARGO_INCREMENTAL = '0'
|
||||||
// 不把节点用户名写进仓库:PATH 在下面的 shell 步骤里按 $HOME 展开。
|
// 不把节点用户名写进仓库:PATH 在下面的 shell 步骤里按 $HOME 展开。
|
||||||
AGC_EXTRA_PATH = '/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
AGC_EXTRA_PATH = '/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
||||||
|
// universal 需要两份**同版本**的架构运行时:宿主(arm64)取自 PATH 里的构建 Node,
|
||||||
|
// x86_64 由节点预装的官方发行版提供。只写相对 HOME 的目录,绝对路径在 shell 步骤里拼。
|
||||||
|
// 节点没有它时构建前即失败,绝不静默跳过或让更新包缺一份运行时。
|
||||||
|
AGC_NODE_RUNTIME_DARWIN_X64_HOME = 'Library/Jenkins/node/darwin-x64'
|
||||||
}
|
}
|
||||||
stages {
|
stages {
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
@@ -116,6 +120,24 @@ pipeline {
|
|||||||
arch -x86_64 /usr/bin/uname -m
|
arch -x86_64 /usr/bin/uname -m
|
||||||
if command -v sccache >/dev/null 2>&1; then sccache --version; else echo '[agc-macos] 未找到 sccache;本次回退到 rustc 直接构建。'; fi
|
if command -v sccache >/dev/null 2>&1; then sccache --version; else echo '[agc-macos] 未找到 sccache;本次回退到 rustc 直接构建。'; fi
|
||||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
|
# 随包 Node 的两套架构运行时:宿主那一份就是 PATH 里的构建 Node,
|
||||||
|
# x86_64 那一份必须是节点上预装的官方发行版(含 LICENSE 与 lib/node_modules/npm)。
|
||||||
|
# 两份版本必须一致,否则 staging 会在写出任何资源前失败关闭;这里提前到编译前暴露。
|
||||||
|
x64_root="${AGC_NODE_RUNTIME_DARWIN_X64_HOME:-Library/Jenkins/node/darwin-x64}"
|
||||||
|
case "$x64_root" in /*) ;; *) x64_root="${HOME:?HOME 不能为空}/$x64_root" ;; esac
|
||||||
|
if [ ! -x "${x64_root}/bin/node" ]; then
|
||||||
|
echo "[agc-macos] 缺少 x86_64 Node 运行时:${x64_root}/bin/node"
|
||||||
|
echo '[agc-macos] 安装官方 darwin-x64 发行版到该目录(保留 LICENSE 与 lib/node_modules/npm)后重试;也可以覆盖 AGC_NODE_RUNTIME_DARWIN_X64_HOME'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
host_node_version="$(node --version)"
|
||||||
|
x64_node_version="$(/usr/bin/arch -x86_64 "${x64_root}/bin/node" --version)"
|
||||||
|
if [ "$host_node_version" != "$x64_node_version" ]; then
|
||||||
|
echo "[agc-macos] 两套 Node 运行时版本不一致:arm64=${host_node_version} x86_64=${x64_node_version}"
|
||||||
|
echo '[agc-macos] universal 更新包只接受同版本的两套运行时,请把节点上的 x86_64 发行版升到与构建 Node 相同版本'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[agc-macos] 随包 Node 运行时:arm64=${host_node_version} x86_64=${x64_node_version}(来源 ${x64_root})"
|
||||||
npm ci --no-audit --no-fund
|
npm ci --no-audit --no-fund
|
||||||
node apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs
|
node apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs
|
||||||
'''
|
'''
|
||||||
@@ -143,6 +165,12 @@ pipeline {
|
|||||||
sh '''
|
sh '''
|
||||||
set -eu
|
set -eu
|
||||||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:${AGC_EXTRA_PATH}"
|
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:${AGC_EXTRA_PATH}"
|
||||||
|
# universal staging 需要另一架构的 Node 来源;路径与 Toolchain 阶段同一口径,
|
||||||
|
# 缺失/版本不一致由 staging 在任何落盘前再次失败关闭。
|
||||||
|
x64_root="${AGC_NODE_RUNTIME_DARWIN_X64_HOME:-Library/Jenkins/node/darwin-x64}"
|
||||||
|
case "$x64_root" in /*) ;; *) x64_root="${HOME:?HOME 不能为空}/$x64_root" ;; esac
|
||||||
|
export AGC_NODE_RUNTIME_DARWIN_X64_PATH="${x64_root}/bin/node"
|
||||||
|
export AGC_NODE_RUNTIME_DARWIN_X64_LICENSE_PATH="${x64_root}/LICENSE"
|
||||||
echo "[agc-macos] 渠道=${AGC_UPDATE_CHANNEL} 分区=${AGC_UPDATE_CHANNEL}-mac 目标=universal-apple-darwin dry-run=${AGC_RELEASE_DRY_RUN}"
|
echo "[agc-macos] 渠道=${AGC_UPDATE_CHANNEL} 分区=${AGC_UPDATE_CHANNEL}-mac 目标=universal-apple-darwin dry-run=${AGC_RELEASE_DRY_RUN}"
|
||||||
ossutil_bin="${OSSUTIL_BIN:-ossutil}"
|
ossutil_bin="${OSSUTIL_BIN:-ossutil}"
|
||||||
if command -v "${ossutil_bin}" >/dev/null 2>&1; then
|
if command -v "${ossutil_bin}" >/dev/null 2>&1; then
|
||||||
|
|||||||
Reference in New Issue
Block a user