Skip to content

Commit 87f99d2

Browse files
Merge branch 'main' into fix/publish-binary-before-npm
Co-authored-by: Cursor <cursoragent@cursor.com>
2 parents 2aba873 + e3828ed commit 87f99d2

5 files changed

Lines changed: 214 additions & 0 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ jobs:
5959
- name: Ensure zip and tar (per-platform binary archives)
6060
run: sudo apt-get update && sudo apt-get install -y zip
6161

62+
# bun build --compile leaves an invalid darwin linker signature. rcodesign
63+
# re-signs those Mach-O files on the Linux runner (binary-codesign.mjs).
64+
- name: Install rcodesign (ad-hoc sign darwin binaries)
65+
run: |
66+
set -euo pipefail
67+
RCODESIGN_VERSION=0.29.0
68+
asset="apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl.tar.gz"
69+
curl -fsSL -o "/tmp/${asset}" \
70+
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign/${RCODESIGN_VERSION}/${asset}"
71+
tar -xzf "/tmp/${asset}" -C /tmp
72+
sudo install -m 755 \
73+
"/tmp/apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl/rcodesign" \
74+
/usr/local/bin/rcodesign
75+
rcodesign --version
76+
6277
- run: pnpm install --frozen-lockfile
6378

6479
# Binary compile uses `bun build --compile` CLI (not Bun.build API).
@@ -119,6 +134,21 @@ jobs:
119134
- name: Ensure zip and tar (per-platform binary archives)
120135
run: sudo apt-get update && sudo apt-get install -y zip
121136

137+
# bun build --compile leaves an invalid darwin linker signature. rcodesign
138+
# re-signs those Mach-O files on the Linux runner (binary-codesign.mjs).
139+
- name: Install rcodesign (ad-hoc sign darwin binaries)
140+
run: |
141+
set -euo pipefail
142+
RCODESIGN_VERSION=0.29.0
143+
asset="apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl.tar.gz"
144+
curl -fsSL -o "/tmp/${asset}" \
145+
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign/${RCODESIGN_VERSION}/${asset}"
146+
tar -xzf "/tmp/${asset}" -C /tmp
147+
sudo install -m 755 \
148+
"/tmp/apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl/rcodesign" \
149+
/usr/local/bin/rcodesign
150+
rcodesign --version
151+
122152
- run: pnpm install --frozen-lockfile
123153

124154
# Binary compile uses `bun build --compile` CLI (not Bun.build API).

‎docs/agents/publish.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
115115
- **GitHub Release**:`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`(stable / channel 均需)
116116
- **Node 版本**:24(npm 11.5+ 才支持 OIDC token 交换)
117117
- **Bun**:`oven-sh/setup-bun`,版本钉死在 workflow 中
118+
- **darwin 签名**:编完后 `binary-codesign.mjs` 对 darwin Mach-O 做 ad-hoc 重签,并按 Apple 的方式重算每一页 SHA-256(末页不补零)。对不上就中止发布。macOS runner 额外跑 `codesign --verify --strict`。Linux runner 用 `rcodesign 0.29.0 sign`;不要用 `rcodesign verify`(会拒绝 Apple 已通过的 ad-hoc 签名)。漏签时 Apple Silicon 会在启动时 SIGKILL
118119
- **Actions 版本**:checkout/setup-node/pnpm-action 均为 v6(Node 24 兼容)
119120
- **npm 配置**:当前 release tooling 发布的包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands` / `bailian-cli` / `knowledge-studio-cli`)的 Trusted Publisher 指向 `modelstudioai/cli` 的 `publish.yml`;新增发布包时同步 npm Trusted Publisher
120121

@@ -161,5 +162,6 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
161162
| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 |
162163
| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 |
163164
| channel job 缺少 `contents: write` | `gh release create` 失败 |
165+
| darwin 二进制编完后没有 ad-hoc 重签 | Apple Silicon / macOS 27 对失效的 linker 签名直接 SIGKILL(`Killed: 9`) |
164166
| npm 先于二进制 / OSS 成功 | latest 已发出,CDN 上没有对应 zip/tar.gz;OSS 网络失败时无法回滚 npm |
165167
| stable 未先推 tag 就建 Release | `--verify-tag` 失败 |

‎tools/release/lib/binary-build.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
rollingManifestChannelId,
2222
rollingManifestFileName,
2323
} from "./binary-options.mjs";
24+
import { signDarwinAdhoc } from "./binary-codesign.mjs";
2425
import { ensureTar, tarOne } from "./binary-tar.mjs";
2526
import { ensureZip, zipOne } from "./binary-zip.mjs";
2627

@@ -166,6 +167,8 @@ function compileOne({ bunTarget, os, arch, exe }, version, outdir, entry) {
166167
if (result.stderr) process.stderr.write(result.stderr);
167168
// Bun 1.2.19 writes windows-x64 .exe with mode 000 on Unix hosts (oven-sh/bun#21308).
168169
chmodSync(innerPath, 0o755);
170+
// Sign after chmod. Bun's linker signature does not match the final Mach-O bytes.
171+
if (os === "darwin") signDarwinAdhoc(innerPath, { log });
169172
return { innerName, innerPath, os, arch, exe };
170173
}
171174

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Ad-hoc re-sign Bun-compiled darwin Mach-O binaries.
3+
*
4+
* `bun build --compile` leaves a linker signature (`Identifier=a.out`,
5+
* `adhoc,linker-signed`) whose page hashes do not match the final file.
6+
* macOS 27 SIGKILLs that binary at launch (exit 137). Re-signing after
7+
* compile covers the bytes that actually ship.
8+
*
9+
* darwin hosts use `codesign`, then `codesign --verify --strict`.
10+
* Linux/Windows release runners use `rcodesign sign`, then check each
11+
* CodeDirectory page hash in-process. `rcodesign verify` is not a gate:
12+
* 0.29.0 rejects ad-hoc signatures Apple accepts (`CMS error`).
13+
*/
14+
import { createHash } from "node:crypto";
15+
import { readFileSync } from "node:fs";
16+
import { spawnSync } from "node:child_process";
17+
18+
export const RCODESIGN_VERSION = "0.29.0";
19+
20+
function defaultLog(message = "") {
21+
process.stdout.write(`${message}\n`);
22+
}
23+
24+
/** Command used to replace the linker signature. `platform` is the build host, not the target. */
25+
export function adhocSignPlan(platform = process.platform) {
26+
if (platform === "darwin") {
27+
return {
28+
command: "codesign",
29+
args: ["--force", "--sign", "-", "--identifier", "bl"],
30+
};
31+
}
32+
return {
33+
command: "rcodesign",
34+
args: ["sign"],
35+
};
36+
}
37+
38+
const LC_CODE_SIGNATURE = 0x1d;
39+
const CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0;
40+
const CSMAGIC_CODEDIRECTORY = 0xfade0c02;
41+
const CSSLOT_CODEDIRECTORY = 0;
42+
const CDHASH_SHA256 = 2;
43+
44+
/**
45+
* Recompute CodeDirectory page hashes the way Apple does for ad-hoc Mach-O:
46+
* each 4KiB page is SHA-256 of the bytes that exist, with no zero padding.
47+
* Bun's linker signature matches the padded last page and fails this check.
48+
*
49+
* @param {string} binaryPath
50+
*/
51+
export function verifyAdhocPageHashes(binaryPath) {
52+
const data = readFileSync(binaryPath);
53+
const magic = data.readUInt32LE(0);
54+
const littleEndian = magic === 0xfeedfacf;
55+
if (!littleEndian && magic !== 0xcffaedfe) {
56+
throw new Error(`${binaryPath} is not a thin 64-bit Mach-O`);
57+
}
58+
const read32 = littleEndian
59+
? (offset) => data.readUInt32LE(offset)
60+
: (offset) => data.readUInt32BE(offset);
61+
const commandCount = read32(16);
62+
let commandOffset = 32;
63+
let signatureOffset = 0;
64+
for (let commandIndex = 0; commandIndex < commandCount; commandIndex++) {
65+
const command = read32(commandOffset);
66+
const commandSize = read32(commandOffset + 4);
67+
if (command === LC_CODE_SIGNATURE) signatureOffset = read32(commandOffset + 8);
68+
commandOffset += commandSize;
69+
}
70+
if (!signatureOffset) throw new Error(`${binaryPath} has no LC_CODE_SIGNATURE`);
71+
const readBlob32 = (offset) => data.readUInt32BE(signatureOffset + offset);
72+
if (readBlob32(0) !== CSMAGIC_EMBEDDED_SIGNATURE) {
73+
throw new Error(`${binaryPath} code signature superblob is invalid`);
74+
}
75+
const blobCount = readBlob32(8);
76+
let directoryOffset = 0;
77+
for (let blobIndex = 0; blobIndex < blobCount; blobIndex++) {
78+
const blobType = readBlob32(12 + blobIndex * 8);
79+
const blobOffset = readBlob32(12 + blobIndex * 8 + 4);
80+
if (blobType === CSSLOT_CODEDIRECTORY) directoryOffset = blobOffset;
81+
}
82+
if (!directoryOffset) throw new Error(`${binaryPath} has no CodeDirectory`);
83+
const directory = signatureOffset + directoryOffset;
84+
if (data.readUInt32BE(directory) !== CSMAGIC_CODEDIRECTORY) {
85+
throw new Error(`${binaryPath} CodeDirectory magic is invalid`);
86+
}
87+
const version = data.readUInt32BE(directory + 8);
88+
const hashOffset = data.readUInt32BE(directory + 16);
89+
const codeSlotCount = data.readUInt32BE(directory + 28);
90+
let codeLimit = data.readUInt32BE(directory + 32);
91+
const hashSize = data[directory + 36];
92+
const hashType = data[directory + 37];
93+
const pageShift = data[directory + 39];
94+
if (version >= 0x20300) {
95+
const codeLimit64 = data.readBigUInt64BE(directory + 48);
96+
if (codeLimit64 > 0n) codeLimit = Number(codeLimit64);
97+
}
98+
if (hashType !== CDHASH_SHA256 || hashSize !== 32) {
99+
throw new Error(`${binaryPath} code signature is not SHA-256`);
100+
}
101+
if (codeLimit > data.length) {
102+
throw new Error(`${binaryPath} codeLimit ${codeLimit} exceeds file length ${data.length}`);
103+
}
104+
const pageSize = pageShift === 0 ? codeLimit : 1 << pageShift;
105+
for (let slot = 0; slot < codeSlotCount; slot++) {
106+
const start = slot * pageSize;
107+
const end = Math.min(start + pageSize, codeLimit);
108+
const recorded = data.subarray(
109+
directory + hashOffset + slot * hashSize,
110+
directory + hashOffset + (slot + 1) * hashSize,
111+
);
112+
const actual = createHash("sha256").update(data.subarray(start, end)).digest();
113+
if (!recorded.equals(actual)) {
114+
throw new Error(
115+
`${binaryPath} code signature page ${slot} does not match the file (stale ad-hoc signature)`,
116+
);
117+
}
118+
}
119+
}
120+
121+
/**
122+
* Replace the Mach-O ad-hoc signature in place.
123+
* Then recompute page hashes so a stale linker signature fails the build on Linux.
124+
* On darwin, also run `codesign --verify --strict`.
125+
*
126+
* @param {string} binaryPath
127+
* @param {{ log?: (message?: string) => void, platform?: NodeJS.Platform }} [options]
128+
*/
129+
export function signDarwinAdhoc(
130+
binaryPath,
131+
{ log = defaultLog, platform = process.platform } = {},
132+
) {
133+
const plan = adhocSignPlan(platform);
134+
log(`adhoc sign via ${plan.command}`);
135+
const result = spawnSync(plan.command, [...plan.args, binaryPath], { encoding: "utf-8" });
136+
if (result.error?.code === "ENOENT") {
137+
const installHint =
138+
platform === "darwin"
139+
? "codesign ships with the Xcode command line tools."
140+
: `Install rcodesign ${RCODESIGN_VERSION} (apple-codesign) and retry.`;
141+
throw new Error(
142+
`${plan.command} not found on PATH. Darwin binaries need an ad-hoc signature before packaging. ${installHint}`,
143+
);
144+
}
145+
if (result.stdout) process.stdout.write(result.stdout);
146+
if (result.stderr) process.stderr.write(result.stderr);
147+
if (result.status !== 0) {
148+
throw new Error(`ad-hoc sign failed for ${binaryPath}`);
149+
}
150+
verifyAdhocPageHashes(binaryPath);
151+
if (platform !== "darwin") return;
152+
153+
const verify = spawnSync("codesign", ["--verify", "--strict", binaryPath], { encoding: "utf-8" });
154+
if (verify.stdout) process.stdout.write(verify.stdout);
155+
if (verify.stderr) process.stderr.write(verify.stderr);
156+
if (verify.status !== 0) {
157+
const detail = (verify.stderr || verify.stdout || "").trim();
158+
throw new Error(
159+
`codesign --verify --strict failed for ${binaryPath}${detail ? `: ${detail}` : ""}`,
160+
);
161+
}
162+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, test } from "vite-plus/test";
2+
import { adhocSignPlan, RCODESIGN_VERSION } from "./binary-codesign.mjs";
3+
4+
describe("darwin ad-hoc sign plan", () => {
5+
test("darwin hosts use codesign and replace the linker signature", () => {
6+
expect(adhocSignPlan("darwin")).toEqual({
7+
command: "codesign",
8+
args: ["--force", "--sign", "-", "--identifier", "bl"],
9+
});
10+
});
11+
12+
test("non-darwin hosts use rcodesign so Linux CI can sign Mach-O", () => {
13+
expect(RCODESIGN_VERSION).toBe("0.29.0");
14+
expect(adhocSignPlan("linux")).toEqual({ command: "rcodesign", args: ["sign"] });
15+
expect(adhocSignPlan("win32")).toEqual({ command: "rcodesign", args: ["sign"] });
16+
});
17+
});

0 commit comments

Comments
 (0)