/** * Host-side `features.json` generation for cross-compiled binaries. * * `features.json` ships next to every CI artifact and is consumed by the * release tooling (packages/bun-release/scripts/upload-s3.ts) and by the * crash-report decoder: a crash-report URL carries the enabled-feature set as * a VLQ-encoded bitset, and the `features` array here maps bit index → name. * Native builds generate it by running the freshly built binary with * `scripts/features.mjs` (→ `crash_handler.getFeatureData()`); a * cross-compiled binary can't run on the build host, but every field is a * build-time constant we already know: * * - `features` — `PACKED_FEATURES_LIST`, a const generated by the * `define_features!` macro in src/analytics/lib.rs. * Parsed out of the source below; indices must match * the binary exactly or crash reports decode wrong. * - `version` — package.json version (cfg.version) * - `is_canary` — cfg.canary * - `revision` — git sha (cfg.revision) * - `generated_at` — wall clock, same as the runtime path * * test/internal/macos-cross-config.test.ts pins the parser against the real * `crash_handler.getFeatureData()` output of a debug build from the same * source tree, so a refactor of the macro that breaks the parser fails CI * rather than silently shipping a wrong feature table. */ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { BuildError } from "./error.ts"; /** * Extract `PACKED_FEATURES_LIST` from the `define_features! { ... }` * invocation in src/analytics/lib.rs without compiling or running anything. * * Each entry has the shape ` => (, "")`. * The macro asserts at compile time that the indices are dense 0..N; the * same check is repeated here so a parse that silently drops an entry (or a * macro reshape the regex doesn't understand) fails the build instead of * producing a misaligned feature table. */ export function parsePackedFeaturesList(cwd: string): string[] { const sourcePath = resolve(cwd, "src", "analytics", "lib.rs"); const source = readFileSync(sourcePath, "utf8"); // The macro now recurses internally (`define_features! { @storage ... }`), // so several invocation-shaped blocks exist; the entry list is the one whose // body contains ` => (...)` arms. Scan every block and keep the // entries we find — the density check below still rejects partial parses. const invocations = [...source.matchAll(/define_features!\s*\{([\s\S]*?)\n\s*\}/g)]; if (invocations.length === 0) { throw new BuildError(`Could not find the define_features! invocation in ${sourcePath}`, { hint: "parsePackedFeaturesList() in scripts/build/features-json.ts needs updating to match the new shape.", }); } const entries: { index: number; name: string }[] = []; // ` => (, "")` with an optional // `, core = IDENT` alias of the bun_core feature static. const entryRe = /(\d+)\s*=>\s*\(\s*\w+\s*,\s*"((?:[^"\\]|\\.)*)"\s*(?:,\s*core\s*=\s*\w+\s*)?\)/g; for (const invocation of invocations) { for (const m of invocation[1]!.matchAll(entryRe)) { entries.push({ index: Number(m[1]), name: m[2]! }); } } if (entries.length === 0) { throw new BuildError(`Parsed zero entries from define_features! in ${sourcePath}`, { hint: "parsePackedFeaturesList() in scripts/build/features-json.ts needs updating to match the new shape.", }); } // The bit index is the entry's position in PACKED_FEATURES_LIST. Sort by // it and require density, mirroring the macro's own const assert. entries.sort((a, b) => a.index - b.index); entries.forEach((e, i) => { if (e.index !== i) { throw new BuildError( `define_features! indices are not dense at ${i} (got ${e.index}, feature "${e.name}") — ` + `either the source is wrong (the macro's const assert would also fail) or the parser ` + `in scripts/build/features-json.ts missed an entry.`, ); } }); return entries.map(e => e.name); } /** * Build the features.json payload for a binary that can't be executed on * the build host. Field set and meaning match `crash_handler.getFeatureData()` * (src/runtime/api/crash_handler_jsc.rs) / scripts/features.mjs. */ export function crossFeaturesJson(cfg: { cwd: string; version: string; canary: boolean; revision: string }): string { return JSON.stringify({ features: parsePackedFeaturesList(cfg.cwd), version: cfg.version, is_canary: cfg.canary, revision: cfg.revision, generated_at: Date.now(), }); }