diff --git a/.gitignore b/.gitignore index 75b1dd422..b351f508e 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,5 @@ packages/coding-agent/bench/boot-baseline.json /bazel-out /bazel-testlogs /bazel-pi +/bazel-oh-my-pi /.bazelrc.user diff --git a/Cargo.toml b/Cargo.toml index 27275989f..18349a7a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,20 @@ strip = true # (see crates/pi-natives/src/crash_handler.rs). panic = "unwind" +# rustc ICEs codegenning `MaybeUninit` — a union with +# ScalarPair backend repr on x86_64-pc-windows-msvc — once the MIR opts that run +# from opt-level 2 upward const-fold it into an `Uninit` operand +# (`rustc_codegen_ssa/src/mir/block.rs`: "codegen_argument: OperandRef(Uninit …) +# invalid for pair argument"). Hit by the rustup toolchain this repo pins +# (nightly-2026-07-28) on the local napi path (`packages/natives` → +# `build:bindings`); Bazel's older pin (nightly/2026-04-29, MODULE.bazel) is +# unaffected, which is why CI never saw it. opt-level 1 keeps those MIR opts off. +# maudio is a thin FFI shim — miniaudio itself is C compiled by `cc` at its own +# optimization level — so the cost is noise. Drop this once the rustup pin moves +# past the fix. `ci` and `local` inherit this override from `release`. +[profile.release.package.maudio] +opt-level = 1 + [profile.ci] inherits = "release" lto = "thin" @@ -61,6 +75,9 @@ split-debuginfo = "unpacked" opt-level = 2 debug = false +[profile.dev.package.maudio] +opt-level = 1 + [workspace.lints.rust] # ────────────────────────────────────────────────────────────────────────────── # Rust Lint Levels diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a15aa5523..fcbb047ad 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,10 @@ - Remote MCP transports now enforce header precedence and origin policy: client-generated HTTP/MCP/authorization headers win over configured headers case-insensitively, and Agent Plugins servers never forward configured headers across a redirect to a different origin (method-changing redirects of JSON-RPC POSTs are refused). Agent Plugins stdio `env` values and remote `headers` are likewise exempt from config-value resolution (no ambient env-name lookup, no `!command` execution, empty values preserved). - Added `omp share `: share a saved session by id prefix or `.jsonl` path without launching the agent — same encrypted upload, store selection, and `share.redactSecrets` handling as the `/share` slash command. +### Fixed + +- Fixed the compiled binary build on Windows: `Bun.Glob.scan` yields backslash-separated paths, which the legacy Pi virtual module used verbatim for export keys and generated identifiers, producing invalid JavaScript. + ## [17.2.10] - 2026-08-06 ### Breaking Changes diff --git a/packages/coding-agent/scripts/legacy-pi-virtual-module.ts b/packages/coding-agent/scripts/legacy-pi-virtual-module.ts index 41abffabe..913a3a934 100644 --- a/packages/coding-agent/scripts/legacy-pi-virtual-module.ts +++ b/packages/coding-agent/scripts/legacy-pi-virtual-module.ts @@ -151,7 +151,10 @@ export async function collectBundledPiEntries(): Promise { const glob = new Bun.Glob(`**/*${pattern.sourceSuffix}`); const matches: string[] = []; for await (const match of glob.scan({ cwd: sourceDir, onlyFiles: true })) { - matches.push(match); + // Bun.Glob yields host separators; the export keys and generated + // identifiers below are `/`-shaped. Same normalization as + // `generate-docs-index.ts`. + matches.push(match.split(path.sep).join("/")); } matches.sort(); for (const match of matches) { diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 04d2f4b98..e02f2145e 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +### Added + +- `bun run build` (`scripts/bazel-natives.ts`) now supports Windows hosts: the `host` pseudo-target delegates to the local napi build against VS Build Tools (the bazel msvc cross toolchain remains linux/mac-only), and other targets fail fast with guidance instead of dying deep inside a bazel repo rule. The local build also auto-appends VS Build Tools' bundled CMake/Ninja to `PATH` via vswhere, so it works outside a vcvars prompt. + +### Fixed + +- Fixed the native addon loader selecting the `baseline` CPU variant on every Windows host whose `powershell.exe` is Windows PowerShell 5.1: `[System.Runtime.Intrinsics.X86.Avx2]` only exists on .NET Core, so the probe never reported AVX2. Detection now calls `IsProcessorFeaturePresent` through `bun:ffi`, which is both correct and ~270 ms cheaper at startup (the PowerShell spawn is gone); Node embeds fall back to `pwsh` before `powershell.exe`. +- Fixed `bun run build:bindings` failing on Windows: the `node_modules/.bin` entry is a `napi.exe` launcher there, which Bun tried to parse as JavaScript. The CLI's JS entry is now resolved from the `@napi-rs/cli` manifest. +- Fixed the local (non-Bazel) addon build always producing the `baseline` variant on Windows — `scripts/host-detect.ts` shared the broken PowerShell AVX2 probe. +- Fixed a rustc ICE building `maudio` for `x86_64-pc-windows-msvc` under the pinned rustup nightly by capping that package at `opt-level = 1`; MIR const-folding turned `MaybeUninit` into an `Uninit` operand that codegen rejects for a ScalarPair argument. + ## [17.2.10] - 2026-08-06 ### Fixed diff --git a/packages/natives/native/loader-state.js b/packages/natives/native/loader-state.js index 385aed862..464eda576 100644 --- a/packages/natives/native/loader-state.js +++ b/packages/natives/native/loader-state.js @@ -324,13 +324,39 @@ function detectAvx2Support() { } if (process.platform === "win32") { - const output = runCommand("powershell.exe", [ - "-NoProfile", - "-NonInteractive", - "-Command", - "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported", - ]); - return output && output.toLowerCase() === "true"; + // Under Bun, ask the kernel: PF_AVX2_INSTRUCTIONS_AVAILABLE == 40. Exact, + // and ~0.5 ms against ~270 ms for the PowerShell spawn it replaces on the + // startup path. + if (typeof Bun !== "undefined") { + try { + const { dlopen, FFIType } = createRequire(import.meta.url)("bun:ffi"); + const kernel32 = dlopen("kernel32.dll", { + IsProcessorFeaturePresent: { args: [FFIType.u32], returns: FFIType.i32 }, + }); + try { + return kernel32.symbols.IsProcessorFeaturePresent(40) !== 0; + } finally { + kernel32.close(); + } + } catch { + // No FFI (embedder policy, unusual host): fall through to the shell probe. + } + } + // Node embeds have no `bun:ffi`. `[System.Runtime.Intrinsics.X86.Avx2]` + // exists only on .NET Core, so `pwsh` (PowerShell 7) answers correctly + // while a stock `powershell.exe` (Windows PowerShell 5.1, .NET Framework) + // raises TypeNotFound and pins such hosts to the baseline addon. + for (const shell of ["pwsh.exe", "powershell.exe"]) { + const output = runCommand(shell, [ + "-NoProfile", + "-NonInteractive", + "-Command", + "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported", + ]); + if (output && output.toLowerCase() === "true") return true; + if (output && output.toLowerCase() === "false") return false; + } + return false; } return false; diff --git a/packages/natives/scripts/build-bindings.ts b/packages/natives/scripts/build-bindings.ts index 8711871aa..0e11ab0e3 100644 --- a/packages/natives/scripts/build-bindings.ts +++ b/packages/natives/scripts/build-bindings.ts @@ -5,7 +5,10 @@ * (`bun run build:bindings`) only when the Rust API changes its exported * typedefs. Host target only, local cargo profile — no cross-compilation. */ + +import * as fsSync from "node:fs"; import * as fs from "node:fs/promises"; +import { createRequire } from "node:module"; import * as path from "node:path"; import { $ } from "bun"; import { detectHostAvx2Support } from "../../../scripts/host-detect"; @@ -20,6 +23,44 @@ process.env.PCRE2_SYS_STATIC ??= "1"; // policy override. process.env.CMAKE_POLICY_VERSION_MINIMUM ??= "3.5"; +// Windows: cc-rs and rustc auto-locate cl.exe/link.exe through the VS +// registry, but the cmake crate (audiopus_sys' bundled opus) needs cmake — +// and its Ninja generator needs ninja — on PATH. VS Build Tools ships both +// without exposing them, so outside a vcvars prompt the build dies on +// "cmake not found". Resolve the VS install via vswhere and append its +// CMake/Ninja dirs, keeping any user-provided tools ahead. +if (process.platform === "win32" && (!Bun.which("cmake") || !Bun.which("ninja"))) { + const vswhere = path.join( + process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", + "Microsoft Visual Studio", + "Installer", + "vswhere.exe", + ); + const probe = Bun.spawnSync( + [ + vswhere, + "-latest", + "-products", + "*", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", + "installationPath", + ], + { stdout: "pipe", stderr: "pipe" }, + ); + const vsRoot = probe.exitCode === 0 ? probe.stdout.toString("utf-8").trim() : ""; + if (vsRoot) { + const cmakeExt = path.join(vsRoot, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake"); + const extraDirs = [path.join(cmakeExt, "CMake", "bin"), path.join(cmakeExt, "Ninja")].filter(dir => + fsSync.existsSync(dir), + ); + if (extraDirs.length > 0) { + process.env.PATH = [process.env.PATH ?? "", ...extraDirs].filter(Boolean).join(path.delimiter); + } + } +} + const repoRoot = path.join(import.meta.dir, "../../.."); const rustDir = path.join(repoRoot, "crates/pi-natives"); const nativeDir = path.join(import.meta.dir, "../native"); @@ -142,18 +183,28 @@ const buildOutputDir = await fs.mkdtemp( path.join(nativeDir, ".build", `${process.platform}-${process.arch}-${effectiveVariant ?? "default"}-local-`), ); -// 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). -const napiBin = Bun.which("napi", { - PATH: [ - path.join(import.meta.dir, "..", "node_modules", ".bin"), - path.join(repoRoot, "node_modules", ".bin"), - process.env.PATH ?? "", - ].join(path.delimiter), -}); -if (!napiBin) { - throw new Error("Could not locate @napi-rs/cli `napi` binary in node_modules/.bin"); +// Resolve the CLI's JS entry from the package manifest rather than the +// `node_modules/.bin` shim: `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), and +// on Windows the shim is a `napi.exe` launcher that Bun would try to parse as +// JavaScript. +const require_ = createRequire(import.meta.url); +const napiManifestPath = require_.resolve("@napi-rs/cli/package.json"); +const napiManifest: unknown = require_(napiManifestPath); +const napiBinEntry = + typeof napiManifest === "object" && + napiManifest !== null && + "bin" in napiManifest && + typeof napiManifest.bin === "object" && + napiManifest.bin !== null && + "napi" in napiManifest.bin && + typeof napiManifest.bin.napi === "string" + ? napiManifest.bin.napi + : null; +if (!napiBinEntry) { + throw new Error(`@napi-rs/cli manifest at ${napiManifestPath} declares no string \`bin.napi\` entry`); } +const napiBin = path.join(path.dirname(napiManifestPath), napiBinEntry); const napiArgs = [ "build", diff --git a/packages/natives/test/native.test.ts b/packages/natives/test/native.test.ts index 4a7033ea5..d4b814057 100644 --- a/packages/natives/test/native.test.ts +++ b/packages/natives/test/native.test.ts @@ -640,7 +640,13 @@ describe("pi-natives", () => { expect(callbackError).toBeNull(); expect(result.exitCode).toBe(0); expect(result.timedOut).toBeFalse(); - expect(JSON.parse(output.trim())).toEqual(expected); + // ConPTY interleaves terminal negotiation with the child's own bytes + // (`ESC[6n`, SGR reset, an OSC 0 title set, cursor show), so strip the + // escape sequences before parsing the payload. + const payload = output + .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)|\u001b\[[0-9;?]*[ -/]*[@-~]/g, "") + .trim(); + expect(JSON.parse(payload)).toEqual(expected); }); it("reports the child PID as soon as the PTY process starts", async () => { diff --git a/scripts/bazel-natives.ts b/scripts/bazel-natives.ts index d93a2bb28..17725627c 100755 --- a/scripts/bazel-natives.ts +++ b/scripts/bazel-natives.ts @@ -18,6 +18,13 @@ * Extra args after `--` are passed to bazel verbatim (cache configs, endpoints, * headers — see .bazelrc for the cache-rw/cache-ro policy configs). * + * Windows hosts: the msvc cc toolchain in bazel/toolchains/msvc only supports + * linux/mac exec hosts (its clang-cl+xwin wrappers replace the MSVC a Windows + * box already has), so a win32 host cannot run any bazel addon build. The + * `host` pseudo-target instead delegates to the local napi build + * (packages/natives/scripts/build-bindings.ts) against the installed VS Build + * Tools; every other target on a win32 host fails fast with guidance. + * * Note: musl addons intentionally reuse the plain linux- filenames, so a * `linux-all` copy overwrites the gnu addon with the musl one (and vice versa); * CI jobs that ship files always request an explicit disjoint target set. @@ -214,10 +221,47 @@ async function installAddon(sourcePath: string, destPath: string): Promise } } +/** + * win32-host path for the `host` pseudo-target: the bazel msvc cross toolchain + * cannot run here, but real MSVC can — build the addon via the napi local + * build and install it into destDir like the bazel path would. + */ +async function buildWindowsHostAddon(host: HostInfo, destDir: string): Promise { + const script = path.join(repoRoot, "packages/natives/scripts/build-bindings.ts"); + console.log(`win32 host: bazel msvc toolchain is linux/mac-only; building via ${path.relative(repoRoot, script)}`); + const proc = Bun.spawn([process.execPath, script], { + cwd: repoRoot, + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await proc.exited; + if (exitCode !== 0) process.exit(exitCode || 1); + + const filename = `pi_natives.win32-x64-${host.avx2 ? "modern" : "baseline"}.node`; + const builtPath = path.join(repoRoot, "packages/natives/native", filename); + if (path.dirname(builtPath) !== destDir) { + await fs.mkdir(destDir, { recursive: true }); + await installAddon(builtPath, path.join(destDir, filename)); + } + console.log(`installed ${filename} → ${path.join(destDir, filename)}`); +} + async function main(): Promise { const options = parseCliArgs(process.argv.slice(2)); const host: HostInfo = { platform: process.platform, arch: process.arch, avx2: detectHostAvx2Support() }; const destDir = options.dest ? path.resolve(options.dest) : path.join(repoRoot, "packages/natives/native"); + + if (host.platform === "win32" && !options.source) { + if (options.targets.length !== 1 || options.targets[0] !== "host") { + throw new Error( + `Cannot bazel-build [${options.targets.join(", ")}] on a Windows host: the msvc cross ` + + "toolchain (bazel/toolchains/msvc) only runs on linux/mac exec hosts. Use `host` here " + + "(local napi build via VS Build Tools), or run this script from WSL/linux for cross targets.", + ); + } + await buildWindowsHostAddon(host, destDir); + return; + } let outputs: string[]; if (options.source) { diff --git a/scripts/host-detect.ts b/scripts/host-detect.ts index 2976a174b..138e3ef3c 100644 --- a/scripts/host-detect.ts +++ b/scripts/host-detect.ts @@ -1,3 +1,4 @@ +import { dlopen, FFIType } from "bun:ffi"; import * as fs from "node:fs"; function runCommand(command: string, args: string[]): string | null { @@ -30,13 +31,23 @@ export function detectHostAvx2Support(): boolean { } if (process.platform === "win32") { - const output = runCommand("powershell.exe", [ - "-NoProfile", - "-NonInteractive", - "-Command", - "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported", - ]); - return output?.toLowerCase() === "true"; + // `[System.Runtime.Intrinsics.X86.Avx2]` only exists on .NET Core, so the + // PowerShell probe reported `false` on every host whose `powershell.exe` + // is Windows PowerShell 5.1 (.NET Framework) — i.e. a stock Windows box — + // silently downgrading AVX2 machines to the baseline ISA. Ask the kernel + // instead: PF_AVX2_INSTRUCTIONS_AVAILABLE == 40. + try { + const kernel32 = dlopen("kernel32.dll", { + IsProcessorFeaturePresent: { args: [FFIType.u32], returns: FFIType.i32 }, + }); + try { + return kernel32.symbols.IsProcessorFeaturePresent(40) !== 0; + } finally { + kernel32.close(); + } + } catch { + return false; + } } return false;