102 lines
4.6 KiB
TypeScript
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(),
|
||
|
|
});
|
||
|
|
}
|