fix(setup): used portable native install artifacts

- Built the generic Windows release binary with Bun's baseline x64 runtime so older Windows 10 CPUs do not hit the AVX2-only modern executable.

- Forced pi-natives release builds to link PCRE2 statically so macOS installs do not depend on Homebrew's libpcre2 dylib.

- Added release dry-run and native-build regression coverage for the portable artifact contracts.

Fixes #5172
This commit is contained in:
roboomp
2026-07-11 09:09:59 +00:00
parent 6328671d1d
commit bc7a143c1e
10 changed files with 116 additions and 19 deletions
+1 -1
View File
@@ -110,7 +110,7 @@
"build:native": "bun --cwd=packages/natives run build",
"test": "bun scripts/ci-test-ts.ts local",
"test:ts": "bun scripts/ci-test-ts.ts local-ts",
"test:scripts": "bun test scripts/ci-concurrency.test.ts scripts/ci-release-notes.test.ts scripts/fix-dts-extensions.test.ts scripts/link-omp.test.ts",
"test:scripts": "bun test scripts/ci-build-native.test.ts scripts/ci-concurrency.test.ts scripts/ci-release-build-binaries.test.ts scripts/ci-release-notes.test.ts scripts/fix-dts-extensions.test.ts scripts/link-omp.test.ts",
"test:rs": "bun scripts/run-rs-task.ts test:rs",
"check": "bun run --parallel check:ts check:rs",
"check:ts": "bun run check:tools && bun run --workspaces --if-present check",
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed native Windows binary installs on older Windows 10 CPUs by building the generic `omp-windows-x64.exe` release asset with Bun's baseline x64 runtime instead of the AVX2-only modern target. ([#5172](https://github.com/can1357/oh-my-pi/issues/5172))
## [16.4.3] - 2026-07-11
### Added
+12 -12
View File
@@ -7,14 +7,16 @@ import { compileCodingAgent } from "./compile-binary";
const packageDir = path.join(import.meta.dir, "..");
const repoRoot = path.join(packageDir, "..", "..");
interface CrossBuild {
/** Binary cross-compilation settings selected by `CROSS_TARGET`. */
export interface CrossBuild {
readonly id: string;
readonly platform: string;
readonly arch: string;
readonly target: Bun.Build.CompileTarget;
}
function resolveCrossBuild(value: string | undefined): CrossBuild | null {
/** Resolves a CROSS_TARGET value to the Bun compile target used by local binary builds. */
export function resolveCrossBuild(value: string | undefined): CrossBuild | null {
switch (value) {
case undefined:
case "":
@@ -29,16 +31,12 @@ function resolveCrossBuild(value: string | undefined): CrossBuild | null {
return { id: value, platform: "linux", arch: "x64", target: "bun-linux-x64-baseline" };
case "win32-x64":
case "windows-x64":
return { id: value, platform: "win32", arch: "x64", target: "bun-windows-x64-modern" };
return { id: value, platform: "win32", arch: "x64", target: "bun-windows-x64-baseline" };
default:
throw new Error(`Unsupported CROSS_TARGET: ${value}`);
}
}
const crossBuild = resolveCrossBuild(Bun.env.CROSS_TARGET);
const outName = crossBuild ? `omp-${crossBuild.id}` : "omp";
const outputPath = path.join(packageDir, "dist", outName);
// Transformers.js is an optional, native-heavy dependency that is never bundled
// into the binary; the tiny-model worker `bun install`s it into a runtime cache
// on first use. The `catalog:` spec cannot be resolved from inside the compiled
@@ -55,7 +53,7 @@ if (
}
const transformersVersion = transformersManifest.version;
function shouldAdhocSignDarwinBinary(): boolean {
function shouldAdhocSignDarwinBinary(crossBuild: CrossBuild | null): boolean {
return process.platform === "darwin" && !crossBuild;
}
@@ -77,6 +75,9 @@ async function runCommand(
}
async function main(): Promise<void> {
const crossBuild = resolveCrossBuild(Bun.env.CROSS_TARGET);
const outName = crossBuild ? `omp-${crossBuild.id}` : "omp";
const outputPath = path.join(packageDir, "dist", outName);
// Generate inside the try so the finally always restores the empty checked-in
// placeholders (stats client archive, docs index) even on failure.
try {
@@ -99,11 +100,10 @@ async function main(): Promise<void> {
transformersVersion,
target: crossBuild?.target,
external: ["fastembed", "onnxruntime-node"],
skipBuiltinCodesign: shouldAdhocSignDarwinBinary(),
skipBuiltinCodesign: shouldAdhocSignDarwinBinary(crossBuild),
});
// Bun 1.3.12 emits a truncated Mach-O signature on darwin builds.
if (shouldAdhocSignDarwinBinary()) {
if (shouldAdhocSignDarwinBinary(crossBuild)) {
await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
}
} finally {
@@ -115,4 +115,4 @@ async function main(): Promise<void> {
}
}
await main();
if (import.meta.main) await main();
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed macOS `pi-natives` package installs depending on Homebrew's dynamic `libpcre2-8.0.dylib`; release builds now force pcre2-sys to link PCRE2 statically. ([#5172](https://github.com/can1357/oh-my-pi/issues/5172))
## [16.4.3] - 2026-07-11
### Fixed
+4
View File
@@ -4,6 +4,10 @@ import { $ } from "bun";
import { detectHostAvx2Support } from "../../../scripts/host-detect";
import { generateEnumExports } from "./gen-enums";
// pcre2-sys prefers a system libpcre2 when pkg-config finds one. Release addons
// must not retain host Homebrew paths such as /opt/homebrew/opt/pcre2/*.dylib.
process.env.PCRE2_SYS_STATIC ??= "1";
const repoRoot = path.join(import.meta.dir, "../../..");
const rustDir = path.join(repoRoot, "crates/pi-natives");
const nativeDir = path.join(import.meta.dir, "../native");
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import { $ } from "bun";
const repoRoot = path.join(import.meta.dir, "..");
async function runCiNativeDryRun(env: Record<string, string | undefined> = {}): Promise<string> {
const result = await $`bun scripts/ci-build-native.ts --dry-run`
.cwd(repoRoot)
.quiet()
.env({
...process.env,
PCRE2_SYS_STATIC: "0",
RUSTFLAGS: "",
TARGET_VARIANT: "",
TARGET_VARIANTS: "",
...env,
})
.nothrow();
expect(result.exitCode).toBe(0);
return result.text();
}
describe("ci native build environment", () => {
it("prints static PCRE2 env for the default native build dry run", async () => {
await expect(runCiNativeDryRun()).resolves.toBe(
"DRY RUN bun --cwd=packages/natives run build [default] PCRE2_SYS_STATIC=1\n",
);
});
it("prints static PCRE2 env without dropping x64 variant settings", async () => {
await expect(runCiNativeDryRun({ TARGET_VARIANTS: "baseline" })).resolves.toBe(
'DRY RUN bun --cwd=packages/natives run build [baseline] PCRE2_SYS_STATIC=1 TARGET_VARIANT=baseline RUSTFLAGS="-C target-cpu=x86-64-v2"\n',
);
});
});
+14 -5
View File
@@ -21,6 +21,13 @@ const variantConfigs: Record<NativeBuildVariant["name"], NativeBuildVariant> = {
},
};
/** Adds release-portability env required by native addon builds. */
export function withPortableNativeBuildEnv(
env: Record<string, string | undefined>,
): Record<string, string | undefined> {
return { ...env, PCRE2_SYS_STATIC: "1" };
}
function parseTargetVariants(): NativeBuildVariant[] {
const rawVariants = (Bun.env.TARGET_VARIANTS ?? "").trim();
if (!rawVariants) return [];
@@ -35,15 +42,17 @@ function parseTargetVariants(): NativeBuildVariant[] {
}
async function runNativeBuild(env: Record<string, string | undefined>, label: string): Promise<void> {
const buildEnv = withPortableNativeBuildEnv(env);
if (isDryRun) {
const variant = env.TARGET_VARIANT ? ` TARGET_VARIANT=${env.TARGET_VARIANT}` : "";
const rustflags = env.RUSTFLAGS ? ` RUSTFLAGS=${JSON.stringify(env.RUSTFLAGS)}` : "";
console.log(`DRY RUN bun --cwd=packages/natives run build [${label}]${variant}${rustflags}`);
const staticPcre = ` PCRE2_SYS_STATIC=${buildEnv.PCRE2_SYS_STATIC}`;
const variant = buildEnv.TARGET_VARIANT ? ` TARGET_VARIANT=${buildEnv.TARGET_VARIANT}` : "";
const rustflags = buildEnv.RUSTFLAGS ? ` RUSTFLAGS=${JSON.stringify(buildEnv.RUSTFLAGS)}` : "";
console.log(`DRY RUN bun --cwd=packages/natives run build [${label}]${staticPcre}${variant}${rustflags}`);
return;
}
console.log(`Building natives [${label}]...`);
await $`bun --cwd=packages/natives run build`.cwd(repoRoot).env(env);
await $`bun --cwd=packages/natives run build`.cwd(repoRoot).env(buildEnv);
}
async function main(): Promise<void> {
@@ -65,4 +74,4 @@ async function main(): Promise<void> {
}
}
await main();
if (import.meta.main) await main();
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import { $ } from "bun";
import { resolveCrossBuild } from "../packages/coding-agent/scripts/build-binary";
const repoRoot = path.join(import.meta.dir, "..");
describe("Windows release binary target", () => {
it("builds the generic Windows release asset with the baseline runtime", async () => {
const result = await $`bun scripts/ci-release-build-binaries.ts --dry-run --targets win32-x64`
.cwd(repoRoot)
.quiet()
.nothrow();
expect(result.exitCode).toBe(0);
const output = result.text();
expect(output).toContain("Building packages/coding-agent/binaries/omp-windows-x64.exe...");
expect(output).toContain(
"DRY RUN Bun.build target=bun-windows-x64-baseline outfile=packages/coding-agent/binaries/omp-windows-x64.exe",
);
expect(output).not.toContain("bun-windows-x64-modern");
});
it("uses the baseline runtime for local Windows cross-build aliases", () => {
expect(resolveCrossBuild("win32-x64")).toEqual({
id: "win32-x64",
platform: "win32",
arch: "x64",
target: "bun-windows-x64-baseline",
});
expect(resolveCrossBuild("windows-x64")).toEqual({
id: "windows-x64",
platform: "win32",
arch: "x64",
target: "bun-windows-x64-baseline",
});
});
});
+1 -1
View File
@@ -63,7 +63,7 @@ const targets: BinaryTarget[] = [
id: "win32-x64",
platform: "win32",
arch: "x64",
target: "bun-windows-x64-modern",
target: "bun-windows-x64-baseline",
outfile: "packages/coding-agent/binaries/omp-windows-x64.exe",
},
];
+2
View File
@@ -111,6 +111,7 @@ const localOnlyWorkspacePackages = ["packages/mnemopi", "python/robomp/web"];
// silently ignores unmatched filters when at least one other filter matches.)
const repoScriptTests = [
"scripts/ci-concurrency.test.ts",
"scripts/ci-build-native.test.ts",
"scripts/ci-release-notes.test.ts",
"scripts/fix-dts-extensions.test.ts",
"scripts/link-omp.test.ts",
@@ -345,6 +346,7 @@ async function commandsForMode(mode: Mode): Promise<TestCommand[]> {
"--parallel=4",
...onlyFailuresArgs,
"scripts/ci-concurrency.test.ts",
"scripts/ci-build-native.test.ts",
"scripts/fix-dts-extensions.test.ts",
],
},