macOS 发布改为只出 arm64 单架构
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled

- build-macos-ci 固定 aarch64-apple-darwin:单架构构建、只跑 arm64 隔离 smoke、首装包命名 <产品名>_<版本>_aarch64.dmg,构建清单记录 smokes 与实际清单平台键
- stage-node-runtime 的 universal-apple-darwin 恢复失败关闭并说明原因:随包 Node 只有单架构官方发行版,按宿主架构放行会让 Intel 上这份侧车不可执行,且通用包自检按 process.arch 校验抓不到
- 守卫用例改为断言入口固定 arm64 目标与 _aarch64.dmg 后缀、universal 目标必须抛错
- Jenkinsfile 与三份技术/运维文档同步单架构口径:清单只登记 darwin-aarch64,Intel 非目标,恢复 universal 的前置条件写在文档里
- 共享记忆补 2026-09-21 决策记录,pitfalls 记下「只带宿主架构的通用包」陷阱,避免再次用过渡实现糊过去
This commit is contained in:
2026-09-21 12:30:56 +08:00
parent a5ee06a4c7
commit e53992fdc2
10 changed files with 72 additions and 67 deletions
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
import {
generateUpdateManifest,
prepareReleaseVersion,
resolveManifestPlatformKeys,
resolveReleaseContext,
resolveReleasePartition,
runTauriBuild,
@@ -20,10 +21,13 @@ import {
} from './verify-updater-signature.mjs';
/**
* AGC macOS 分区(`<channel>-mac`)发布入口:构建 universal 包 → 双架构 smoke → 生成 universal DMG
* AGC macOS 分区(`<channel>-mac`)发布入口:构建 arm64 单架构包 → arm64 隔离 smoke → 生成 arm64 DMG
* → 生成分区清单 latest.json → 用产物内烘焙的公钥验签 → 按 dry-run 决定是否上传 OSS。
*
* 边界:
* - 只出 Apple Siliconarm64)单架构:清单只登记 `darwin-aarch64`。Intel 侧要可用,前提是随包 Node
* 也能按架构各带一份(`stage-node-runtime.mjs` 对 universal 目标失败关闭);在实现之前**不得**
* 把 arm64 产物登记成 `darwin-x86_64`,否则 Intel 客户端会装到跑不起来的包。
* - Apple 签名与公证暂缺:本入口剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名,但**不能传
* `--no-sign`** —— 该标志同时会跳过 updater 的 minisign 签名,产物就没有 `.sig`
* 未签名 + 未公证必须显式记录而非静默通过;
@@ -94,11 +98,14 @@ process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const dryRun = readReleaseDryRun();
process.env.CARGO_TARGET_DIR = path.join(appRoot, 'src-tauri/target');
const context = resolveReleaseContext(['--target=universal-apple-darwin']);
// 单架构目标:清单侧 `resolveManifestPlatformKeys` 只为它登记 darwin-aarch64。
const macTarget = 'aarch64-apple-darwin';
const context = resolveReleaseContext([`--target=${macTarget}`]);
const partition = resolveReleasePartition(context.channel, context.target);
const version = await prepareReleaseVersion(context);
// 首装包名必须保持 `<产品名>_<版本>_universal.dmg`:清单侧按该后缀唯一匹配本次产物。
const firstInstallName = `${productName}_${version}_universal.dmg`;
// 首装包名必须让清单侧的单架构分支唯一匹配:`<产品名>_<版本>_<架构>.dmg`
// 架构段用 Tauri 的 aarch64 口径(不是 updater 平台键的 arm64 / x86_64)。
const firstInstallName = `${productName}_${version}_aarch64.dmg`;
// 幂等边界:workspace 会保留上一轮产物。先删掉本次将要写出的对象,否则
// 1) hdiutil 会因同名 DMG 已存在直接失败(首次实跑即命中);
@@ -117,7 +124,7 @@ for (const stale of [
}
const args = [
'--target=universal-apple-darwin',
`--target=${macTarget}`,
'--bundles',
'app',
'--ci',
@@ -132,16 +139,13 @@ const command = (binary, argv, options = {}) =>
runTauriBuild(args, context);
const app = path.join(context.bundleRoot, 'macos', appBundleName);
for (const architecture of ['arm64', 'x86_64']) {
command(process.execPath, [
path.join(appRoot, 'scripts/check-macos-bundle.mjs'),
app,
architecture,
'--universal',
]);
}
command(process.execPath, [
path.join(appRoot, 'scripts/check-macos-bundle.mjs'),
app,
'arm64',
]);
// DMG 放在 bundle 根目录下:渠道清单的首装包选择会扫描该目录,命名必须匹配 `_<version>_universal.dmg`。
// DMG 放在 bundle 根目录下:渠道清单的首装包选择会扫描该目录,命名必须匹配 `_<version>_aarch64.dmg`。
const dmgDirectory = path.join(context.bundleRoot, 'macos');
fs.mkdirSync(dmgDirectory, { recursive: true });
const dmg = path.join(dmgDirectory, firstInstallName);
@@ -170,7 +174,7 @@ const release = await generateUpdateManifest(context);
assert.equal(
path.resolve(release.downloadArtifact),
path.resolve(dmg),
'首装包必须锁定本次生成的 universal DMG',
'首装包必须锁定本次生成的 arm64 DMG',
);
// 上传前门禁:用产物里烘焙的公钥复核更新包签名。验不过就停在这里,绝不写 OSS。
@@ -266,8 +270,9 @@ fs.writeFileSync(
firstInstallSha256: dmgHash,
manifest: 'latest.json',
},
smokes: ['arm64', 'x86_64'],
intelSmoke: process.arch === 'arm64' ? 'Rosetta' : 'native',
// 单架构发布:只跑 arm64 隔离 smokeIntel 未支持(清单里没有 darwin-x86_64 键)。
smokes: ['arm64'],
manifestPlatformKeys: resolveManifestPlatformKeys(context.target),
},
null,
2,
@@ -178,8 +178,12 @@ test('macOS release entry and smoke script derive product names from config and
assert.ok(entry.includes('readProductName'), '入口必须从 Tauri 配置读产品名');
assert.ok(!entry.includes('陶泥儿'), 'macOS 发布入口不得写死产品名');
assert.ok(
entry.includes('_${version}_universal.dmg'),
'首装包名必须保留清单侧唯一匹配所需的后缀',
entry.includes("const macTarget = 'aarch64-apple-darwin'"),
'macOS 发布入口必须固定单架构目标',
);
assert.ok(
entry.includes('_${version}_aarch64.dmg'),
'首装包名必须保留清单侧单架构分支唯一匹配所需的后缀(Tauri 口径 aarch64',
);
const smoke = fs.readFileSync(
@@ -59,27 +59,23 @@ export function readInstalledNodeLicense(
return result.content;
}
export function targetRuntime(
target,
{ platform = process.platform, arch = process.arch } = {},
) {
export function targetRuntime(target) {
const targets = {
'x86_64-pc-windows-msvc': ['win32', 'x64'],
'aarch64-apple-darwin': ['darwin', 'arm64'],
'x86_64-apple-darwin': ['darwin', 'x64'],
};
if (target === 'universal-apple-darwin') {
// Tauri 通用包含 arm64+x86_64 两个 slice,而这里的 Node 便携运行时是
// 「构建宿主架构」的单架构发行版:macOS 构建机(当前为 arm64)只能提供
// 自身架构的官方 Node。真正的通用 Node 需要另行下载另一架构发行版并
// 用 lipo 合并,属未完成项;在此之前 non-darwin 宿主必须失败关闭
if (platform !== 'darwin') {
throw new Error(
`Node 运行时不支持${platform}/${arch} 宿主构建通用 macOS 包:${target}`,
);
}
return { platform: 'darwin', arch };
}
// 随包 Node 是**单架构**官方发行版:一份运行时只服务它自己的架构。
// macOS 发布当前固定为 aarch64-apple-darwin 单架构包(Intel 未支持),
// universal 目标没有正确的运行时来源,必须失败关闭——绝不能退化成
// 「按宿主架构暂存一份 arm64」:那样通用包自检(按 process.arch)能过,
// 但 Intel 机器上这份运行时不可执行,用户拿到的是坏包
if (target === 'universal-apple-darwin')
throw new Error(
'Node 运行时不支持 universal-apple-darwin:随包 Node 只有单架构发行版,' +
'通用包需按架构各带一份(另行下载另一架构官方发行版)之后才能构建;' +
'当前 macOS 发布固定为 aarch64-apple-darwin 单架构',
);
const value = targets[target];
if (!value) throw new Error(`Node 运行时不支持发布目标:${target}`);
return { platform: value[0], arch: value[1] };
@@ -281,20 +281,10 @@ test('native target and macOS dynamic dependency policy reject nonportable Node'
platform: 'darwin',
arch: 'arm64',
});
assert.deepEqual(
targetRuntime('universal-apple-darwin', {
platform: 'darwin',
arch: 'arm64',
}),
{ platform: 'darwin', arch: 'arm64' },
);
// universal 必须失败关闭:只带宿主架构那一份运行时,Intel 上不可执行。
assert.throws(
() =>
targetRuntime('universal-apple-darwin', {
platform: 'linux',
arch: 'x64',
}),
/不支持在 linux\/x64 宿主构建通用 macOS 包/u,
() => targetRuntime('universal-apple-darwin'),
/universal-apple-darwin/u,
);
assertPortableMacNode(
'/node:\n\t/usr/lib/libSystem.B.dylib (compatibility version 1)\n',