Files
2026-08-27 21:09:14 +00:00

102 lines
4.6 KiB
TypeScript

/**
* 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 `<index> => (<rust_ident>, "<feature name>")`.
* 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 `<index> => (...)` 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 }[] = [];
// `<index> => (<rust_ident>, "<feature name>")` 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(),
});
}