diff --git a/Cargo.lock b/Cargo.lock index caa27c96c..bc2ebcd2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2722,6 +2722,7 @@ version = "14.1.0" dependencies = [ "arboard", "ast-grep-core", + "blake3", "brush-builtins", "brush-core", "bytes", diff --git a/REPORT-ISSUE-601.md b/REPORT-ISSUE-601.md new file mode 100644 index 000000000..a91e2733d --- /dev/null +++ b/REPORT-ISSUE-601.md @@ -0,0 +1,793 @@ +# Issue 601 report + +SSOT for `https://github.com/can1357/oh-my-pi/issues/601`. + +Date: `2026-04-09` +Repo: `/root/projects/project-oh-my-pi-fork` +Branch: `research/native-illegal-instruction` +Base during investigation: `upstream/main @ d350ea60ef2f006945a9da3ce510fbe23093a779` +Reporter on issue: `@MikeeI` +Goal: preserve all verified findings, strongest hypotheses, dead ends, local experiments, and next steps so follow-on agents do not restart from zero. + +## Bottom line + +- Visible symptom: typing non-empty `@` mention in TUI crashes `omp` with `Illegal instruction`. +- Narrow trigger: `@` enters native `fuzzyFind()` path. +- Root defect almost certainly below TUI and below Bun: broken Linux `x64` native addon artifacts. +- Strongest currently defensible statement: + 1. published Linux `x64` artifacts for `@oh-my-pi/pi-natives` are ISA-unsafe on at least some releases + 2. installed `linux-x64-modern` and `linux-x64-baseline` artifacts on this machine contain AVX-512 markers + 3. direct Node loading of installed `.node` also exits `132` + 4. `@` mention autocomplete only exposes that native crash +- High-value maintainer work: inspect native artifact production/release path, not TUI surgery. + +## Confidence map + +### Confirmed + +- Non-empty `@` mention reaches native `fuzzyFind()`. +- Installed published Linux `x64` artifacts on this machine contain AVX-512 markers. +- Installed published artifact crashes under direct Node load; not Bun-only. +- Existing repo-local `pi_natives.linux-x64-modern.node` on this machine does not show AVX-512 markers and does not crash. +- `v13.17.6 -> v13.18.0` changed no relevant source in native/TUI/CI path. +- `v13.18.0 -> v13.19.0` changed no relevant source in native/TUI/CI path. +- Therefore issue timeline `good -> bad -> good` is not explained by git-tracked source deltas in those windows. +- Local investigation found one real build-pipeline defect: `zlob` host Zig builds default to `native`, bypassing intended x64 ISA contract. +- Local investigation found one experiment-corrupting hazard in `build-native.ts`: stale canonical `.node` files in `packages/natives/native/` can be reused and mistaken for fresh build output. + +### Strong but not fully proven + +- Release artifacts are nondeterministically wrong or stale-artifact contaminated. +- At least one defect sits in release/build/package flow, not only runtime loading. +- `v14.0.2` likely resurfaced latent native artifact problems by reworking variant build/upload flow. + +### Unproven + +- Exact release job / exact machine state that produced bad npm artifacts. +- Whether bad ISA comes only from `zlob`, from another native dependency, from linker/LTO, or from artifact reuse. +- Whether current upstream source, in a truly clean release-like environment, still emits bad `modern`, bad `baseline`, or both. + +## User-visible timeline + +Issue thread facts from `#601`: + +- Reporter: crash reproducible typing `@` then letter; first reported on `13.18.0`. +- Comment: `13.17.6` did not crash. +- Comment: `13.19.0` seemed fixed. +- Comment: `14.0.2` regressed / issue back. + +Resolved tag commits: + +- `v13.17.6` -> `6ef48e02a694eb5deb91bbe7ac8716bcc9cd9261` +- `v13.18.0` -> `3f60ba602abead98563fdab1d9d3db6b28cef429` +- `v13.19.0` -> `e52535c51cc2147f8d1ae2d6e25f92a771ce1df6` +- `v14.0.2` -> `8f8ced7d77a53a6bbc56366cacd0d5ac70cefe8e` +- `v14.0.3` -> `fd67efc046543bb5c65a81fca6c638b57f491dfd` + +Regression-hunt result: + +- `v13.17.6..v13.18.0`: no relevant changes in `packages/natives`, `packages/tui`, `.github/workflows/ci.yml`, native release scripts; only package version bumps for `pi-natives` and `pi-tui`. +- `v13.18.0..v13.19.0`: same; no relevant native/TUI/CI logic change, only version bumps. +- Consequence: source history alone does not explain `13.17.6 good / 13.18.0 bad / 13.19.0 good`. +- Highest-confidence interpretation: release artifacts or release environment varied; not a simple code regression in that narrow window. + +## Repro path in source + +Current upstream source path: + +- `packages/tui/src/autocomplete.ts` + +Relevant control flow: + +```ts +const atPrefix = this.#extractAtPrefix(textBeforeCursor); +if (atPrefix) { + const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix); + const suggestions = + rawPrefix.length > 0 + ? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix }) + : await this.#getFileSuggestions("@"); +} +``` + +```ts +const scopedQuery = await this.#resolveScopedFuzzyQuery(query); +const searchPath = scopedQuery?.baseDir ?? this.#basePath; +const fuzzyQuery = scopedQuery?.query ?? query; +const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath), this.#searchDb); +``` + +Conclusion: + +- Bare `@`: does not hit native fuzzy path. +- `@`: does hit native `fuzzyFind()`. +- TUI bug report is accurate at trigger level, misleading at root-cause level. + +## Core evidence + +### 1. Direct Node load also crashes + +Command: + +```bash +node -e 'const mod = require("/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-modern.node"); mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }).then(res => console.log(JSON.stringify(res))).catch(err => { console.error(err); process.exit(1); });' +``` + +Observed: + +- exit code `132` +- no JS exception first + +Meaning: + +- root failure below Bun, below TUI +- native addon alone sufficient to reproduce + +### 2. Published installed artifacts contain AVX-512 markers + +Command used during investigation: + +```bash +python3 - <<'PY' +import subprocess, re +files = { + 'local_repo_modern': '/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-modern.node', + 'global_installed_modern': '/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-modern.node', + 'global_installed_baseline': '/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-baseline.node', +} +for name, path in files.items(): + proc = subprocess.run(['objdump','-d',path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + text = proc.stdout + zmm = len(re.findall(r'\bzmm\d+\b', text)) + mask = len(re.findall(r'\bk[0-7]\b', text)) + print(f'{name}: zmm={zmm} mask={mask}') +PY +``` + +Observed: + +```text +local_repo_modern: zmm=0 mask=0 +global_installed_modern: zmm=5298 mask=1203 +global_installed_baseline: zmm=5298 mask=1203 +``` + +Representative installed disassembly snippet: + +```text +60ba1df: c4 c1 78 92 c9 kmovw %r9d,%k1 +``` + +Meaning: + +- installed published `modern` bad +- installed published `baseline` also bad +- `baseline` showing AVX-512 markers rules out simple `modern too aggressive` story + +### 3. Existing repo-local modern artifact works + +Command: + +```bash +node -e 'const mod = require("/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-modern.node"); mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }).then(res => console.log(JSON.stringify({ ok: true, count: res.matches.length, first: res.matches[0] ?? null }))).catch(err => { console.error(err); process.exit(1); });' +``` + +Observed: + +```json +{"ok":true,"count":5,"first":{"path":"a0629736a5db3561c8b46bdc4b3ce835/","isDirectory":true,"score":110}} +``` + +Caveat: + +- this artifact pre-existed in local workspace; not a clean latest rebuild during investigation +- still strong proof that same machine can run a safe `modern` addon + +## Current source intent vs published reality + +Current loader/runtime intent: + +- `baseline` should mean `x86-64-v2` +- `modern` should mean `x86-64-v3` +- runtime picks `modern` when AVX2 available, else `baseline` + +Relevant file: + +- `packages/natives/native/index.js` + +Relevant logic: + +```js +function resolveCpuVariant(override) { + if (process.arch !== "x64") return null; + if (override) return override; + return detectAvx2Support() ? "modern" : "baseline"; +} +``` + +```js +function getAddonFilenames(tag, variant) { + const defaultFilename = `pi_natives.${tag}.node`; + if (process.arch !== "x64" || !variant) return [defaultFilename]; + const baselineFilename = `pi_natives.${tag}-baseline.node`; + const modernFilename = `pi_natives.${tag}-modern.node`; + if (variant === "modern") { + return [modernFilename, baselineFilename, defaultFilename]; + } + return [baselineFilename, defaultFilename]; +} +``` + +Published reality on this machine: + +- both installed Linux `x64` variants contain AVX-512 markers +- published artifacts do not match source-level ISA contract + +## Earlier commits that introduced risk surface + +These commits matter because they created variant-specific x64 release logic and ISA-sensitive builds. They do not, alone, explain the issue timeline. + +### `81a270fcd` `ci(workflows): added RUSTFLAGS configuration for x86-64-v3 CPU optimization in CI and native builds` + +Effects: + +- CI sets `RUSTFLAGS=-C target-cpu=x86-64-v3` for x64 jobs +- local build script defaults host builds to `target-cpu=native` when `RUSTFLAGS` unset + +Risk: + +- source of ISA-sensitive behavior entered CI and local build path + +### `aa68afc2f` `ci(workflows): updated Bun binary build targets to use modern variants` + +Effects: + +- compiled binary release targets switched to `bun-linux-x64-modern` and `bun-windows-x64-modern` + +Risk: + +- modern/baseline split became operational in release pipeline + +### `e9ab0354f` `feat: added CPU variant support for x64 native addons with AVX2/baseline fallback` + +Effects: + +- introduced `TARGET_VARIANT` +- introduced `modern` / `baseline` filenames +- introduced runtime CPU variant selection + +Risk: + +- build/release/packaging complexity increased sharply + +### `e928b7834` `refactor(natives): migrated native bindings to NAPI-RS with auto-generated types` + +Effects: + +- moved build flow to `napi build` +- changed output normalization logic and native packaging shape + +Risk: + +- another large source of release/build nondeterminism and output-file normalization mistakes + +### `8f8ced7d7` `fix(ci): accept napi plain platform filename and pass TARGET_VARIANTS` + +Effects: + +- `v14.0.2` +- explicitly builds variant matrix with `TARGET_VARIANTS` +- changed native artifact normalization/upload flow again + +Interpretation: + +- strongest candidate for why issue visibly returned in `14.0.2` +- not strong candidate for original `13.18.0` appearance because it is much later + +## New finding: one real upstream build defect in native dependency path + +Dependency: + +- Cargo crate `zlob v1.3.0` +- file read during investigation: `/root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zlob-1.3.0/build.rs` + +Relevant behavior in `build.rs`: + +- if `target == host` and target not Windows, it sets Zig target to `native` +- then runs `zig build` +- this can compile the Zig static library for host-native ISA even when Rust addon itself targets `x86-64-v2` or `x86-64-v3` + +Relevant snippet: + +```rs +let zig_target = if target == host && !target.contains("windows") { + "native" +} else { + rust_target_to_zig(&target) +}; +``` + +Meaning: + +- Rust `RUSTFLAGS=-C target-cpu=x86-64-v2/v3` does not automatically constrain host-built Zig dependencies +- at least one real root-cause path existed in build graph + +## New finding: stale artifact reuse hazard in current build script + +File: + +- `packages/natives/scripts/build-native.ts` + +Current logic: + +```ts +async function resolveBuiltAddonPath(canonicalFilename: string): Promise { + const entries = await fs.readdir(nativeDir); + + if (entries.includes(canonicalFilename)) { + return path.join(nativeDir, canonicalFilename); + } + + const generatedCandidates = entries.filter(entry => { + if (!entry.startsWith(`pi_natives.${targetPlatform}-${targetArch}`) || !entry.endsWith(".node")) { + return false; + } + return !siblingVariantFilenames.has(entry); + }); + ... +} +``` + +Meaning: + +- if `packages/natives/native/pi_natives.-.node` already exists, script treats it as current build output before checking what `napi build` actually emitted +- stale local artifact can be silently reused +- rebuild experiments can lie unless canonical `.node` files are removed first or output dir is isolated + +Importance: + +- high for follow-on investigators +- explains why some local rebuild results are lower-confidence than they first appeared +- may or may not explain CI release bug directly; CI fresh checkouts reduce this risk, local/dev/repeated builds do not + +Experiment hygiene rule from this finding: + +- do not trust any rebuild result while old `.node` files remain in `packages/natives/native/` +- use isolated output dir or ensure canonical files deleted before build + +## Local branch work done during investigation + +Uncommitted exploratory hardening added on this branch: + +- `packages/natives/scripts/build-native.ts` +- `packages/natives/scripts/zig-safe-wrapper.ts` +- `scripts/ci-release-verify-natives.ts` +- `packages/natives/test/build-safety.test.ts` + +Purpose: + +- force host Zig dependency builds to explicit `x86_64_v2` / `x86_64_v3` contract +- add release-time `objdump` guard for AVX-512 markers +- add focused tests for wrapper/guard logic + +What this patch proves: + +- `zlob` host-native Zig path is real and preventable +- release verifier can reliably fail on known-bad published artifacts + +What this patch does not yet prove: + +- full issue fixed +- fresh clean `modern` artifact safe in all cases +- fresh clean `baseline` artifact safe in all cases + +## Verification done on local branch + +### Focused tests + +Command: + +```bash +bun test packages/natives/test/build-safety.test.ts +``` + +Observed: + +- `5 pass` + +### Workspace typecheck + +Command: + +```bash +bun check:ts +``` + +Observed: + +- pass + +### Verify installed published package with new ISA guard + +Command: + +```bash +PI_NATIVE_VERIFY_DIR=/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native bun scripts/ci-release-verify-natives.ts +``` + +Observed: + +- script reports all expected native files present +- then fails with: + +```text +pi_natives.linux-x64-baseline.node contains AVX-512 markers; x86-64-v2 artifacts must stay below x86-64-v4. +pi_natives.linux-x64-modern.node contains AVX-512 markers; x86-64-v3 artifacts must stay below x86-64-v4. +``` + +Meaning: + +- issue reproduced through artifact-level guard, not only runtime crash + +## Local rebuild experiments after Zig-wrapper hardening + +### Build environment repaired first + +Initial blocker: + +- `zlob` bindgen failed: `fatal error: 'stddef.h' file not found` + +Mitigation used locally: + +- installed `clang`, `libc6-dev`, `build-essential` +- downloaded Zig `0.15.2` to `/root/tools/zig-x86_64-linux-0.15.2` + +### First baseline build after wrapper patch + +Command: + +```bash +PATH=/root/tools/zig-x86_64-linux-0.15.2:$PATH CI=1 TARGET_VARIANT=baseline RUSTFLAGS='-C target-cpu=x86-64-v2' bun --cwd=packages/natives run build +``` + +Observed important line: + +```text +Normalizing native addon filename: pi_natives.linux-x64-gnu.node → pi_natives.linux-x64-baseline.node +``` + +Meaning: + +- this particular baseline artifact came from fresh `napi` output normalization, not only stale canonical reuse + +Subsequent `objdump` count on baseline after this run: + +```text +/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-baseline.node: zmm=5270 mask=38 +``` + +Meaning: + +- even after constraining host Zig path, a fresh baseline artifact still showed AVX-512 markers +- therefore `zlob native target` was not the whole problem + +### Modern build after wrapper patch + +Command: + +```bash +PATH=/root/tools/zig-x86_64-linux-0.15.2:$PATH CI=1 TARGET_VARIANT=modern RUSTFLAGS='-C target-cpu=x86-64-v3' bun --cwd=packages/natives run build +``` + +Subsequent count: + +```text +/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-modern.node: zmm=0 mask=0 +``` + +Caution: + +- `build-native.ts` stale-canonical logic can reuse existing `pi_natives.linux-x64-modern.node` +- because canonical `modern` file already existed in `packages/natives/native/`, this safe result is lower-confidence than baseline result +- treat `modern safe after wrapper` as suggestive, not conclusive + +### Later clean attempts + +Commands used: + +```bash +cargo clean -p zlob -p pi-natives +cargo clean +``` + +Why these are not decisive: + +- `cargo clean` clears Cargo targets, not `packages/natives/native/*.node` +- stale-canonical reuse path remains unless canonical `.node` files in `native/` are removed or output redirected + +## New finding after isolated rebuilds + +> This section supersedes the earlier simplistic reading of `any AVX-512 marker == broken build`. + +### CPU capability on this machine + +- `/proc/cpuinfo` shows no `avx512` flags on this host. + +### Exact runtime result of isolated latest-version rebuilds + +After hardening `build-native.ts` to isolate `CARGO_TARGET_DIR` and remove preexisting addon outputs, both latest-version Linux `x64` variants were rebuilt locally from current source. + +Commands used: + +```bash +PATH=/root/tools/zig-x86_64-linux-0.15.2:$PATH CI=1 TARGET_VARIANT=baseline RUSTFLAGS='-C target-cpu=x86-64-v2' bun --cwd=packages/natives run build +PATH=/root/tools/zig-x86_64-linux-0.15.2:$PATH CI=1 TARGET_VARIANT=modern RUSTFLAGS='-C target-cpu=x86-64-v3' bun --cwd=packages/natives run build +``` + +Direct runtime checks on rebuilt local artifacts: + +```bash +node -e 'const mod = require("/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-baseline.node"); mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }).then(res => console.log(JSON.stringify({ ok: true, count: res.matches.length }))).catch(err => { console.error(err); process.exit(1); });' +node -e 'const mod = require("/root/projects/project-oh-my-pi-fork/packages/natives/native/pi_natives.linux-x64-modern.node"); mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }).then(res => console.log(JSON.stringify({ ok: true, count: res.matches.length }))).catch(err => { console.error(err); process.exit(1); });' +bun -e 'import mod from "./packages/natives/native/index.js"; const res = await mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }); console.log(JSON.stringify({ ok: true, count: res.matches.length }));' +bun -e 'import { CombinedAutocompleteProvider } from "./packages/tui/src/autocomplete.ts"; const provider = new CombinedAutocompleteProvider([], "/tmp"); const line = "@a"; const result = await provider.getSuggestions([line], 0, line.length); console.log(JSON.stringify({ ok: true, count: result?.items.length ?? 0 }));' +``` + +Observed: + +- rebuilt local `baseline`: no crash +- rebuilt local `modern`: no crash +- rebuilt local package under Bun: no crash +- exact autocomplete path `CombinedAutocompleteProvider.getSuggestions(["@a"])`: no crash, returned suggestions +- installed published `modern` package still crashes with exit `132` on the same machine + +Meaning: + +- latest-version local rebuilds appear to fix the actual crash trigger on this machine +- the original artifact-level verifier heuristic (`any AVX-512 marker => fail`) is too coarse + +### Why the original AVX-512 verifier was too coarse + +Marker counts after isolated rebuild: + +```text +local baseline: zmm=5270 mask=38 +local modern: zmm=5270 mask=38 +installed modern: zmm=5298 mask=1203 # earlier installed-package measurement +installed baseline: zmm=5298 mask=1203 # earlier installed-package measurement +``` + +New deeper finding: + +- local rebuilt artifacts still contain some AVX-512 instructions +- but they no longer crash on this non-AVX512 machine in the tested `fuzzyFind` / `@a` path +- installed published artifact contains many more AVX-512 clusters and does crash + +Cluster analysis of AVX-512 instruction ranges: + +- local rebuilt `modern`: 5 clusters +- installed published `modern`: 117 clusters + +Mnemonic distribution difference: + +- local rebuilt `modern`: mostly `vpaddd`, `vpxord`, `vprord`, `vpbroadcastd`, limited mask ops +- installed published `modern`: same core block plus many extra `vpcmpeqb`, `vpcmpneqb`, `kortestd`, `kortestw`, `vptestnmb`, `vpblendmb`, `kord`, `kmovd` + +Interpretation: + +- some AVX-512 in local rebuilds is likely benign dispatch-gated code +- installed artifact contains many additional suspicious AVX-512 blocks not present in local rebuild +- therefore the real issue is closer to `unexpected extra AVX-512 code entered published artifact` than `published artifact contains any AVX-512 bytes at all` + +### Source of local rebuilt AVX-512 markers + +Scanning static libs in isolated baseline build showed only two marker-bearing libraries: + +```text +5308 .../release/deps/libfff_search-*.a +5308 .../release/build/blake3-*/out/libblake3_avx512_assembly.a +``` + +Meaning: + +- local rebuilt AVX-512 markers come from `fff-search` / `blake3` AVX-512 assembly inclusion +- this explains why `any marker == broken` is too strict +- next verifier revision must distinguish expected dispatch-gated assembly from the additional suspicious AVX-512 blocks seen in bad published artifacts + +### Updated best interpretation + +- build isolation + stale-output removal + host-Zig ISA pinning were real fixes +- those changes appear sufficient to remove the crashing published-artifact behavior in local latest-version rebuilds on this machine +- remaining work is proving the exact difference between safe local rebuild and bad published artifact, then encoding that difference as a durable release guard + +## Final fix candidate from dependency tracing + +Goal decision: shipped Linux `x64` addons should contain zero AVX-512 instructions. + +Dependency trace: + +```text +pi-natives -> fff-search -> blake3 +``` + +Key finding from source inspection: + +- `blake3` exposes a Cargo feature `no_avx512`, but `blake3-1.8.4/build.rs` does not use it when deciding whether to compile AVX-512 assembly +- therefore enabling `no_avx512` alone does not remove AVX-512 object code from final artifacts +- `blake3` feature `pure` does work for this use case because `build.rs` checks `is_pure()` and skips AVX-512 build steps + +Tested fix in current branch: + +- added direct `blake3 = { version = "1.8.4", default-features = false, features = ["std", "pure"] }` dependency in `crates/pi-natives/Cargo.toml` +- rationale: Cargo feature unification forces `fff-search`'s transitive `blake3` build onto the `pure` path without vendoring `fff-search` or `blake3` + +Observed result after isolated rebuilds: + +```text +pi_natives.linux-x64-baseline.node zmm=0 mask=0 +pi_natives.linux-x64-modern.node zmm=0 mask=0 +``` + +Runtime verification after this dependency change: + +- direct Node load of rebuilt local `baseline` + `fuzzyFind()` succeeds +- exact Bun autocomplete path for `@a` succeeds +- verifier script passes for rebuilt local linux-x64 artifacts + +This is the first configuration in this investigation that simultaneously gives: + +- isolated current-source rebuilds +- zero AVX-512 in final Linux `x64` addons +- no crash on the actual `fuzzyFind` / `@a` path + +Current best fix recommendation: + +1. keep build isolation / stale-output removal / per-variant target dirs +2. keep host-Zig ISA pinning +3. keep artifact-level verify gate +4. force transitive `blake3` onto `pure` for `pi-natives` builds unless/until upstream provides a compile-time-respected `no_avx512` path +5. publish fresh release from latest source with these changes + +Potential follow-up refinement: + +- if maintainers want AVX2/SSE assembly back while still forbidding AVX-512, upstream `blake3` likely needs a patch so `no_avx512` affects build.rs assembly selection +- until then, `pure` is the simplest durable zero-AVX512 fix + +## Best current explanation + +Most plausible multi-factor explanation: + +1. x64 variant support introduced ISA-sensitive build/release complexity +2. release path produced or selected unsafe artifacts for some releases +3. installed published Linux `x64` artifacts contain AVX-512 markers under both `modern` and `baseline` +4. issue timeline `13.17.6 good / 13.18.0 bad / 13.19.0 good / 14.0.2 bad again` points to artifact nondeterminism or stale artifact reuse, not simple source regression in those narrow windows +5. at least one true build bug existed in dependency graph: host Zig `native` +6. at least one more problem remains: fresh baseline build still too new, or experiment path still contaminated, or both + +## What this report rules out + +- pure TUI bug +- pure Bun bug +- simple `modern only` ISA issue +- clean source-only regression between `13.17.6` and `13.18.0` +- clean source-only fix between `13.18.0` and `13.19.0` + +## Highest-value next steps + +### Priority 1: artifact-truth before theory + +- reproduce `v13.18.0`, `v13.19.0`, `v14.0.2`, `v14.0.3` native release builds in isolated clean environments +- for each version, inspect final Linux `x64` `.node` files with `objdump` +- record whether `baseline` / `modern` contain `zmm` or `k[0-7]` + +### Priority 2: eliminate stale-output lies from experiments + +- make build output dir isolated per run, or delete canonical `.node` files before build +- fix `resolveBuiltAddonPath()` so existing canonical file cannot masquerade as fresh build output + +### Priority 3: separate compiler vs packaging failures + +- inspect raw `napi build` outputs before normalization/rename +- hash raw outputs and final renamed outputs +- verify whether both published filenames were copied from same build lineage + +### Priority 4: inspect release workflow statefulness + +Targets: + +- `.github/workflows/ci.yml` +- `scripts/ci-build-native.ts` +- `scripts/ci-release-verify-natives.ts` +- release publish/staging steps + +Questions: + +- same workspace used for multiple variant builds? +- previous variant outputs left in `packages/natives/native/`? +- upload/download artifact merge steps able to overwrite or mix variants? +- publish packaging able to include stale local `.node` files? + +### Priority 5: inspect baseline-specific contamination + +Because fresh baseline looked bad even after Zig wrapper: + +- identify which object files/symbols contain AVX-512 markers in fresh baseline build +- determine whether they originate from `zlob`, another dependency, or linker-combined output +- compare `objdump -d` / `nm -an` / section ownership between bad baseline and safe modern/local artifacts + +## Commands already useful + +### Crash installed published artifact directly + +```bash +node -e 'const mod = require("/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-modern.node"); mod.fuzzyFind({ query: "a", path: "/tmp", maxResults: 5, hidden: true, gitignore: true, cache: true }).then(res => console.log(res));' +``` + +Expected on affected machines: + +- exit `132` + +### Count AVX-512 markers + +```bash +python3 - <<'PY' +import subprocess, re +for path in [ + '/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-modern.node', + '/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native/pi_natives.linux-x64-baseline.node', +]: + out = subprocess.run(['objdump', '-d', path], check=True, stdout=subprocess.PIPE, text=True).stdout + zmm = len(re.findall(r'\bzmm\d+\b', out)) + mask = len(re.findall(r'\bk[0-7]\b', out)) + print(path, zmm, mask) +PY +``` + +### Verify installed package with guard script + +```bash +PI_NATIVE_VERIFY_DIR=/root/.bun/install/global/node_modules/@oh-my-pi/pi-natives/native bun scripts/ci-release-verify-natives.ts +``` + +### Show no relevant source diff in reported good/bad/good windows + +```bash +git diff --name-only v13.17.6..v13.18.0 +git diff --name-only v13.18.0..v13.19.0 +``` + +### Show risk-introducing commits + +```bash +git show e9ab0354f -- .github/workflows/ci.yml packages/natives scripts +git show 81a270fcd -- .github/workflows/ci.yml packages/natives/scripts/build-native.ts +git show 8f8ced7d7 -- .github/workflows/ci.yml scripts/ci-build-native.ts packages/natives/scripts/build-native.ts +``` + +## Open questions + +- Which exact release job produced bad `13.18.0` artifacts? +- Why did `13.19.0` appear good with no relevant source change in native/TUI/CI path? +- Are bad published `baseline` and `modern` identical, near-identical, or merely both unsafe? +- Is baseline bad because of compiler output, dependency output, link output, or stale output reuse? +- Does `modern` truly become safe after host Zig pinning, or was that local result contaminated by stale canonical reuse? +- Which exact symbols / object files in bad baseline carry AVX-512? +- Is `resolveBuiltAddonPath()` stale reuse only a local experiment hazard, or did a comparable statefulness leak into release jobs too? + +## External references + +- Issue `#601`: `https://github.com/can1357/oh-my-pi/issues/601` +- x86-64 microarchitecture levels, including `x86-64-v4` / AVX-512: `https://en.opensuse.org/X86-64_microarchitecture_levels` +- Zig build-system target/cpu options: `https://zig.guide/build-system/zig-build/` +- Zig build-system overview: `https://ziglang.org/learn/build-system/` +- Example of AVX-512 mismatch causing illegal instruction elsewhere: `https://github.com/jax-ml/jax/issues/2906` +- Recent Node issue showing AVX-512 runtime hazards outside this project: `https://github.com/nodejs/node/issues/53426` + +## Handoff note + +If another agent continues from here: + +- start from this report, not from issue comments alone +- distrust local rebuilds until stale canonical `.node` reuse is eliminated +- focus on artifact provenance and release-state contamination first +- treat TUI changes as mitigation only, not root fix diff --git a/crates/pi-natives/Cargo.toml b/crates/pi-natives/Cargo.toml index eff91e928..aa36f19ee 100644 --- a/crates/pi-natives/Cargo.toml +++ b/crates/pi-natives/Cargo.toml @@ -32,6 +32,8 @@ fff-grep = "0.4" fff = { package = "fff-search", version = "0.4.2", default-features = false, features = [ "zlob", ] } +# Force fff-search's transitive blake3 build off AVX-512 so shipped x64 addons stay <= x86-64-v3. +blake3 = { version = "1.8.4", default-features = false, features = ["std", "pure"] } grep-matcher = "0.1" globset = "0.4" ignore = "0.4" diff --git a/packages/natives/scripts/build-native.ts b/packages/natives/scripts/build-native.ts index 6d3e45ba8..7ff24f608 100644 --- a/packages/natives/scripts/build-native.ts +++ b/packages/natives/scripts/build-native.ts @@ -16,6 +16,13 @@ const isCrossCompile = Boolean(crossTarget) || targetPlatform !== process.platfo type X64Variant = "modern" | "baseline"; +interface SafeHostZigBuildConfig { + wrapperPath: string; + realZigPath: string; + target: string; + cpu: string; +} + let configuredVariant: X64Variant | undefined; if (configuredVariantRaw) { if (targetArch !== "x64") { @@ -77,11 +84,34 @@ function resolveEffectiveVariant(): X64Variant | null { } return detectHostAvx2Support() ? "modern" : "baseline"; } - const effectiveVariant = resolveEffectiveVariant(); const variantSuffix = effectiveVariant ? `-${effectiveVariant}` : ""; -// Default to native CPU optimization for local builds; explicit variants use fixed ISA targets. +function resolveSafeHostZigBuildConfig(): SafeHostZigBuildConfig | null { + if (isCrossCompile || targetArch !== "x64" || !effectiveVariant) { + return null; + } + + if (targetPlatform !== "linux" && targetPlatform !== "darwin") { + return null; + } + + const realZigPath = Bun.which("zig"); + if (!realZigPath) { + return null; + } + + return { + wrapperPath: path.join(import.meta.dir, "zig-safe-wrapper.ts"), + realZigPath, + target: targetPlatform === "linux" ? "x86_64-linux-gnu" : "x86_64-macos", + cpu: effectiveVariant === "modern" ? "x86_64_v3" : "x86_64_v2", + }; +} + +// Keep host-built Zig dependencies on the same ISA floor as the Rust addon. +// zlob's build.rs defaults host builds to `native`, which can leak newer CPU +// instructions into release artifacts even when Rust itself targets x86-64-v2/v3. if (!isCrossCompile && !Bun.env.RUSTFLAGS) { if (effectiveVariant === "modern") { Bun.env.RUSTFLAGS = "-C target-cpu=x86-64-v3"; @@ -161,39 +191,31 @@ async function patchGeneratedIndexLoader(): Promise { await Bun.write(indexPath, content); } -async function resolveBuiltAddonPath(canonicalFilename: string): Promise { - // Variant-tagged files produced by previous invocations of this script that - // should NOT be treated as this build's output (unless they equal our target). - const siblingVariantFilenames = new Set([ - `pi_natives.${targetPlatform}-${targetArch}-modern.node`, - `pi_natives.${targetPlatform}-${targetArch}-baseline.node`, - ]); - siblingVariantFilenames.delete(canonicalFilename); - - const entries = await fs.readdir(nativeDir); - - if (entries.includes(canonicalFilename)) { - return path.join(nativeDir, canonicalFilename); - } - +async function resolveBuiltAddonPath(outputDir: string, canonicalFilename: string): Promise { // napi-rs 3.x emits `${binaryName}.${platformArchABI}.node` where // platformArchABI is e.g. `darwin-x64`, `linux-x64-gnu`, `win32-x64-msvc`, - // `darwin-arm64`. Match any file for this platform/arch that isn't a - // sibling variant we might have produced previously. + // `darwin-arm64`. Build into an isolated output dir so only this invocation's + // outputs are considered fresh candidates. + const entries = await fs.readdir(outputDir); + + if (entries.includes(canonicalFilename)) { + return path.join(outputDir, canonicalFilename); + } + const generatedCandidates = entries.filter(entry => { if (!entry.startsWith(`pi_natives.${targetPlatform}-${targetArch}`) || !entry.endsWith(".node")) { return false; } - return !siblingVariantFilenames.has(entry); + return true; }); if (generatedCandidates.length === 1) { - return path.join(nativeDir, generatedCandidates[0]); + return path.join(outputDir, generatedCandidates[0]); } if (generatedCandidates.length === 0) { throw new Error( - `napi build succeeded but did not emit a native addon for ${targetPlatform}-${targetArch}. Expected ${canonicalFilename} or an environment-tagged variant in ${nativeDir}. Directory contents: ${entries.join(", ") || "(empty)"}.`, + `napi build succeeded but did not emit a native addon for ${targetPlatform}-${targetArch}. Expected ${canonicalFilename} or an environment-tagged variant in ${outputDir}. Directory contents: ${entries.join(", ") || "(empty)"}.`, ); } @@ -203,8 +225,48 @@ async function resolveBuiltAddonPath(canonicalFilename: string): Promise ); } +function resolveBuildOutputDirPrefix(profileLabel: string): string { + const buildTarget = crossTarget ?? `${targetPlatform}-${targetArch}`; + const variantLabel = effectiveVariant ?? "default"; + return path.join(nativeDir, ".build", `${buildTarget}-${variantLabel}-${profileLabel}-`); +} + +async function installGeneratedBindings(outputDir: string): Promise { + for (const filename of ["index.js", "index.d.ts"]) { + const sourcePath = path.join(outputDir, filename); + const destPath = path.join(nativeDir, filename); + try { + await fs.copyFile(sourcePath, destPath); + } catch (err) { + const errno = err as NodeJS.ErrnoException; + if (errno.code === "ENOENT") { + const destExists = await Bun.file(destPath).exists(); + if (destExists) { + continue; + } + } + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to install generated ${filename}: ${message}`); + } + } +} + +function resolveManagedCargoTargetDir(profileLabel: string): string | null { + if (Bun.env.CARGO_TARGET_DIR) { + return null; + } + + const buildTarget = crossTarget ?? `${targetPlatform}-${targetArch}`; + const variantLabel = effectiveVariant ?? "default"; + return path.join(repoRoot, "target", "napi-build", `${buildTarget}-${variantLabel}-${profileLabel}`); +} + const isCI = Boolean(Bun.env.CI); const useLocalProfile = !isCI && !isCrossCompile; +const profileLabel = useLocalProfile ? "local" : "release"; +const profileSuffix = useLocalProfile ? " (local)" : ""; + +const buildOutputDirPrefix = resolveBuildOutputDirPrefix(profileLabel); // Build napi args const napiArgs = [ @@ -218,7 +280,7 @@ const napiArgs = [ "--dts", "index.d.ts", "-o", - nativeDir, + "", ]; if (useLocalProfile) { @@ -229,14 +291,16 @@ if (useLocalProfile) { if (crossTarget) napiArgs.push("--target", crossTarget); -const profileLabel = useLocalProfile ? " (local)" : ""; const canonicalAddonFilename = `pi_natives.${targetPlatform}-${targetArch}${variantSuffix}.node`; const canonicalAddonPath = path.join(nativeDir, canonicalAddonFilename); -console.log(`Building pi-natives for ${targetPlatform}-${targetArch}${variantSuffix}${profileLabel}…`); +console.log(`Building pi-natives for ${targetPlatform}-${targetArch}${variantSuffix}${profileSuffix}…`); await fs.mkdir(nativeDir, { recursive: true }); await cleanupStaleTemps(nativeDir); +await fs.mkdir(path.join(nativeDir, ".build"), { recursive: true }); +const buildOutputDir = await fs.mkdtemp(buildOutputDirPrefix); +napiArgs[10] = buildOutputDir; // Resolve napi bin directly: `bunx @napi-rs/cli` can pick up the wrong bin on // systems where `cli` exists on PATH (e.g. Mono's /usr/bin/cli on Ubuntu). @@ -246,21 +310,44 @@ const napiBin = Bun.which("napi", { if (!napiBin) { throw new Error("Could not locate @napi-rs/cli `napi` binary in node_modules/.bin"); } -const buildResult = await $`${napiBin} ${napiArgs}`.nothrow(); -if (buildResult.exitCode !== 0) { - const stderr = buildResult.stderr?.toString("utf-8") ?? ""; - throw new Error(`napi build failed${stderr ? `:\n${stderr}` : ""}`); + +const managedCargoTargetDir = resolveManagedCargoTargetDir(profileLabel); +if (managedCargoTargetDir) { + Bun.env.CARGO_TARGET_DIR = managedCargoTargetDir; + console.log(`Using isolated CARGO_TARGET_DIR: ${managedCargoTargetDir}`); } -const builtAddonPath = await resolveBuiltAddonPath(canonicalAddonFilename); -if (builtAddonPath !== canonicalAddonPath) { - console.log(`Normalizing native addon filename: ${path.basename(builtAddonPath)} → ${canonicalAddonFilename}`); - await installBinary(builtAddonPath, canonicalAddonPath); - await fs.unlink(builtAddonPath).catch(() => {}); +const safeHostZigBuildConfig = resolveSafeHostZigBuildConfig(); +if (safeHostZigBuildConfig) { + Bun.env.ZIG = safeHostZigBuildConfig.wrapperPath; + Bun.env.PI_NATIVE_REAL_ZIG = safeHostZigBuildConfig.realZigPath; + Bun.env.PI_NATIVE_ZIG_TARGET = safeHostZigBuildConfig.target; + Bun.env.PI_NATIVE_ZIG_CPU = safeHostZigBuildConfig.cpu; + console.log( + `Pinning host Zig CPU contract: ${safeHostZigBuildConfig.target} ${safeHostZigBuildConfig.cpu} (${effectiveVariant})`, + ); } -// Generate runtime enum exports from const enums in index.d.ts -await $`bun ${path.join(import.meta.dir, "gen-enums.ts")}`; -await patchGeneratedIndexLoader(); +try { + const buildResult = await $`${napiBin} ${napiArgs}`.nothrow(); + if (buildResult.exitCode !== 0) { + const stderr = buildResult.stderr?.toString("utf-8") ?? ""; + throw new Error(`napi build failed${stderr ? `:\n${stderr}` : ""}`); + } -console.log("Build complete."); + const builtAddonPath = await resolveBuiltAddonPath(buildOutputDir, canonicalAddonFilename); + if (builtAddonPath !== canonicalAddonPath) { + console.log(`Normalizing native addon filename: ${path.basename(builtAddonPath)} → ${canonicalAddonFilename}`); + await installBinary(builtAddonPath, canonicalAddonPath); + } + + await installGeneratedBindings(buildOutputDir); + + // Generate runtime enum exports from const enums in index.d.ts + await $`bun ${path.join(import.meta.dir, "gen-enums.ts")}`; + await patchGeneratedIndexLoader(); + + console.log("Build complete."); +} finally { + await fs.rm(buildOutputDir, { recursive: true, force: true }); +} diff --git a/packages/natives/scripts/zig-safe-wrapper.ts b/packages/natives/scripts/zig-safe-wrapper.ts new file mode 100755 index 000000000..0d33469d5 --- /dev/null +++ b/packages/natives/scripts/zig-safe-wrapper.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env bun + +export function buildZigArgs(args: string[], overrides: { target?: string; cpu?: string }): string[] { + const nextArgs = [...args]; + if (nextArgs[0] !== "build") { + return nextArgs; + } + + const target = overrides.target?.trim(); + const cpu = overrides.cpu?.trim(); + + if (target && !nextArgs.some(arg => arg.startsWith("-Dtarget="))) { + nextArgs.push(`-Dtarget=${target}`); + } + if (cpu && !nextArgs.some(arg => arg.startsWith("-Dcpu="))) { + nextArgs.push(`-Dcpu=${cpu}`); + } + + return nextArgs; +} + +export async function main( + argv: string[] = process.argv.slice(2), + env: NodeJS.ProcessEnv = process.env, +): Promise { + const realZigPath = env.PI_NATIVE_REAL_ZIG; + if (!realZigPath) { + throw new Error("PI_NATIVE_REAL_ZIG is required when using zig-safe-wrapper.ts"); + } + + const child = Bun.spawn( + [realZigPath, ...buildZigArgs(argv, { target: env.PI_NATIVE_ZIG_TARGET, cpu: env.PI_NATIVE_ZIG_CPU })], + { + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }, + ); + return await child.exited; +} + +if (import.meta.main) { + const exitCode = await main(); + process.exit(exitCode); +} diff --git a/packages/natives/test/build-safety.test.ts b/packages/natives/test/build-safety.test.ts new file mode 100644 index 000000000..59ff38956 --- /dev/null +++ b/packages/natives/test/build-safety.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "bun:test"; +import { hasAvx512Markers } from "../../../scripts/ci-release-verify-natives"; +import { buildZigArgs } from "../scripts/zig-safe-wrapper"; + +describe("native build safety", () => { + describe("buildZigArgs", () => { + it("pins host zig build to the requested cpu contract", () => { + expect( + buildZigArgs(["build", "-Doptimize=ReleaseFast"], { target: "x86_64-linux-gnu", cpu: "x86_64_v2" }), + ).toEqual(["build", "-Doptimize=ReleaseFast", "-Dtarget=x86_64-linux-gnu", "-Dcpu=x86_64_v2"]); + }); + + it("does not override explicit zig target or cpu flags", () => { + expect( + buildZigArgs(["build", "-Dtarget=x86_64-linux-gnu", "-Dcpu=x86_64_v3"], { + target: "x86_64-linux-gnu", + cpu: "x86_64_v2", + }), + ).toEqual(["build", "-Dtarget=x86_64-linux-gnu", "-Dcpu=x86_64_v3"]); + }); + + it("leaves non-build zig commands untouched", () => { + expect(buildZigArgs(["version"], { target: "x86_64-linux-gnu", cpu: "x86_64_v2" })).toEqual(["version"]); + }); + }); + + describe("hasAvx512Markers", () => { + it("flags AVX-512 register markers in disassembly", () => { + expect(hasAvx512Markers("60ba1df:\tc4 c1 78 92 c9\t\tkmovw %r9d,%k1")).toBe(true); + expect(hasAvx512Markers("123456:\t62 f1 7d 48 6f c0\tvmovdqa32 %zmm0,%zmm1")).toBe(true); + }); + + it("flags EVEX-encoded AVX-512 even without zmm or mask registers", () => { + expect(hasAvx512Markers("401000:\t62 f3 75 28 25 c2 96\tvpternlogd $0x96,%ymm2,%ymm1,%ymm0")).toBe(true); + }); + + it("ignores ordinary x86-64 disassembly", () => { + expect(hasAvx512Markers("401000:\t48 89 e5\t\tmov %rsp,%rbp")).toBe(false); + expect(hasAvx512Markers("401004:\tc5 f5 fe c2\tvpaddd %ymm2,%ymm1,%ymm0")).toBe(false); + expect(hasAvx512Markers("58b83d7:\t62 00 00 00 ")).toBe(false); + }); + }); +}); diff --git a/scripts/ci-build-native.ts b/scripts/ci-build-native.ts index b732e26ab..ed3131797 100644 --- a/scripts/ci-build-native.ts +++ b/scripts/ci-build-native.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun +import * as fs from "node:fs"; import * as path from "node:path"; import { $ } from "bun"; @@ -10,6 +11,8 @@ interface NativeBuildVariant { const repoRoot = path.join(import.meta.dir, ".."); const isDryRun = process.argv.includes("--dry-run"); +const targetPlatform = Bun.env.TARGET_PLATFORM || process.platform; +const targetArch = Bun.env.TARGET_ARCH || process.arch; const variantConfigs: Record = { baseline: { name: "baseline", @@ -34,6 +37,60 @@ function parseTargetVariants(): NativeBuildVariant[] { }); } +function runCommand(command: string, args: string[]): string | null { + try { + const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) return null; + return result.stdout.toString("utf-8").trim(); + } catch { + return null; + } +} + +function detectHostAvx2Support(): boolean { + if (process.arch !== "x64") return false; + + if (process.platform === "linux") { + try { + const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8"); + return /\bavx2\b/i.test(cpuInfo); + } catch { + return false; + } + } + + if (process.platform === "darwin") { + const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]); + if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true; + const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]); + return Boolean(features && /\bAVX2\b/i.test(features)); + } + + if (process.platform === "win32") { + const output = runCommand("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported", + ]); + return output?.toLowerCase() === "true"; + } + + return false; +} + +function resolveExpectedAddons(variants: NativeBuildVariant[]): string[] { + if (variants.length > 0) { + return variants.map(variant => `${targetPlatform}-${targetArch}-${variant.name}`); + } + + if (targetArch === "x64") { + return [`${targetPlatform}-${targetArch}-${detectHostAvx2Support() ? "modern" : "baseline"}`]; + } + + return [`${targetPlatform}-${targetArch}`]; +} + async function runNativeBuild(env: Record, label: string): Promise { if (isDryRun) { const variant = env.TARGET_VARIANT ? ` TARGET_VARIANT=${env.TARGET_VARIANT}` : ""; @@ -46,10 +103,25 @@ async function runNativeBuild(env: Record, label: st await $`bun --cwd=packages/natives run build`.cwd(repoRoot).env(env); } +async function verifyBuiltAddons(expectedAddons: string[]): Promise { + if (isDryRun) { + console.log(`DRY RUN bun scripts/ci-release-verify-natives.ts PI_NATIVE_EXPECTED_ADDONS=${expectedAddons.join(" ")}`); + return; + } + + await $`bun scripts/ci-release-verify-natives.ts` + .cwd(repoRoot) + .env({ + ...Bun.env, + PI_NATIVE_EXPECTED_ADDONS: expectedAddons.join(" "), + }); +} + async function main(): Promise { const variants = parseTargetVariants(); if (variants.length === 0) { await runNativeBuild(Bun.env, "default"); + await verifyBuiltAddons(resolveExpectedAddons([])); return; } @@ -63,6 +135,8 @@ async function main(): Promise { variant.name, ); } + + await verifyBuiltAddons(resolveExpectedAddons(variants)); } await main(); diff --git a/scripts/ci-release-verify-natives.ts b/scripts/ci-release-verify-natives.ts index 50a8ccdaa..a72659787 100644 --- a/scripts/ci-release-verify-natives.ts +++ b/scripts/ci-release-verify-natives.ts @@ -4,8 +4,8 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; const repoRoot = path.join(import.meta.dir, ".."); -const nativeDir = path.join(repoRoot, "packages", "natives", "native"); -const expectedAddons = [ +const nativeDir = path.resolve(Bun.env.PI_NATIVE_VERIFY_DIR ?? path.join(repoRoot, "packages", "natives", "native")); +const defaultExpectedAddons = [ "linux-x64-modern", "linux-x64-baseline", "linux-arm64", @@ -15,11 +15,58 @@ const expectedAddons = [ "win32-x64-modern", "win32-x64-baseline", ] as const; +const x64LinuxIsaContracts = [ + { addon: "linux-x64-baseline", filename: "pi_natives.linux-x64-baseline.node", label: "x86-64-v2" }, + { addon: "linux-x64-modern", filename: "pi_natives.linux-x64-modern.node", label: "x86-64-v3" }, +] as const; +const AVX512_REGISTER_PATTERN = /\bzmm\d+\b|\bk[0-7]\b/; + +export function hasAvx512Markers(disassembly: string): boolean { + return disassembly.split("\n").some(line => { + if (AVX512_REGISTER_PATTERN.test(line)) { + return true; + } + + const columns = line.split("\t"); + if (columns.length < 3) { + return false; + } + + const bytes = columns[1]?.trim() ?? ""; + const instruction = columns[2]?.trim() ?? ""; + return bytes.startsWith("62 ") && /^[a-z]/i.test(instruction) && !instruction.startsWith(".byte"); + }); +} + +function disassemble(binaryPath: string): string { + const objdumpPath = Bun.which("objdump"); + if (!objdumpPath) { + throw new Error("objdump is required to verify linux-x64 native ISA contracts."); + } + + const result = Bun.spawnSync([objdumpPath, "-d", binaryPath], { stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) { + const stderr = result.stderr.toString("utf-8").trim(); + throw new Error(`objdump failed for ${binaryPath}${stderr ? `:\n${stderr}` : ""}`); + } + + return result.stdout.toString("utf-8"); +} + +function resolveExpectedAddons(): string[] { + const configured = (Bun.env.PI_NATIVE_EXPECTED_ADDONS ?? "").trim(); + if (!configured) { + return [...defaultExpectedAddons]; + } + + return configured.split(/[\s,]+/).filter(Boolean); +} async function main(): Promise { const entries = await fs.readdir(nativeDir); + const expectedAddons = resolveExpectedAddons(); - console.log("Native addons downloaded:"); + console.log(`Native addons downloaded from ${nativeDir}:`); for (const entry of entries.sort((a, b) => a.localeCompare(b))) { console.log(` ${entry}`); } @@ -37,6 +84,30 @@ async function main(): Promise { for (const platform of expectedAddons) { console.log(`OK pi_natives.${platform}.node`); } + + const isaFailures: string[] = []; + for (const contract of x64LinuxIsaContracts) { + if (!expectedAddons.includes(contract.addon)) { + continue; + } + + const binaryPath = path.join(nativeDir, contract.filename); + const disassembly = disassemble(binaryPath); + if (hasAvx512Markers(disassembly)) { + isaFailures.push(`${contract.filename} contains AVX-512 markers; ${contract.label} artifacts must stay below x86-64-v4.`); + continue; + } + console.log(`OK ${contract.filename} contains no AVX-512 markers`); + } + + if (isaFailures.length > 0) { + for (const failure of isaFailures) { + console.error(failure); + } + process.exit(1); + } } -await main(); +if (import.meta.main) { + await main(); +}