Files
scriptc/packages/compiler/src/backend/native-tools.ts
T
Chris Tate 9179a881a4 Skip the helper version probe and unchanged files' digests on rebuilds
Every native build started the LLVM helper once for `version` before
emitting, and hashed every file the frontend read, including the supplied
declaration files that do not change between builds.

- verifyNativeHelper takes an optional record of helpers that already
  answered `version` as expected. The native compiler keeps it in its cache
  (family "helper"), keyed by the helper's and the compiler's file identity,
  the compiler version and the target. The probe runs again when the helper
  file changes. The package name and version are still checked every time.
- FrontendInputTracker takes the snapshot of the cache entry a build
  replaces. A file whose settled identity has not changed since that
  snapshot keeps the digest recorded there, which is the trust
  frontendInputsStillMatch already gives an unchanged identity.

Builds produce the same executables.
2026-10-27 03:24:16 -05:00

153 lines
4.8 KiB
TypeScript

/** Native tool invocation without a JavaScript host or shell command construction. */
import type { NativeOptimization } from "./optimization.js";
import { execFileSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import type { NativeHelperSpec, NativeTargetSpec } from "./targets.js";
import {
nativePartitionPaths,
validateNativeCodegenVersion,
type NativeCodegenOutputKind,
} from "./native-codegen-core.js";
import {
nativeProfileHelperArgs,
parseNativeProfileMatch,
type NativeProfileMatch,
type NativeProfileOptions,
} from "./native-profile.js";
export function runNativeTool(
executable: string,
args: string[],
cwd?: string,
env?: NodeJS.ProcessEnv,
): string {
return execFileSync(executable, args, {
encoding: "utf8",
stdio: "pipe",
maxBuffer: 64 * 1024 * 1024,
cwd: cwd ?? process.cwd(),
env: env ?? process.env,
});
}
export function requireNativeArtifact(path: string): void {
const info = statSync(path);
if (!info.isFile() || info.size === 0)
throw new Error(`native tool produced no artifact: ${path}`);
}
export function emitNativeObject(options: {
executable: string;
packageRoot: string;
compilerVersion: string;
target: NativeTargetSpec;
helper: NativeHelperSpec;
inputPath: string;
outputPath: string;
sourcePath: string;
optimization: NativeOptimization;
outputKind?: NativeCodegenOutputKind;
/** Program partitions, written to nativePartitionPaths(outputPath). */
partitions?: number;
/** With several partitions: optimize the module whole and divide only its
* code generation among them. */
splitCodegen?: boolean;
/** Private directory where the helper reuses partition artifacts. */
partitionCache?: string;
/** Profile-guided optimization: instrumentation or an indexed profile. */
profile?: NativeProfileOptions;
verified?: NativeHelperVerification;
}): { outputs: string[]; profileMatch: NativeProfileMatch | null } {
verifyNativeHelper(options);
const outputs = nativePartitionPaths(options.outputPath, options.partitions ?? 1);
const stdout = runNativeTool(options.executable, [
"emit",
"--input",
options.inputPath,
...outputs.flatMap((output) => ["--output", output]),
"--filetype",
options.outputKind ?? "obj",
"--target",
options.target.llvmTriple,
"--opt-level",
options.optimization === "dev" ? "0" : "2",
"--relocation-model",
options.target.relocationModel,
"--diagnostic-format",
"json",
"--source-path",
options.sourcePath,
...(options.splitCodegen === true && outputs.length > 1
? ["--split", "codegen"]
: options.partitionCache === undefined || outputs.length < 2
? []
: ["--cache-dir", options.partitionCache]),
...nativeProfileHelperArgs(options.profile),
]);
for (const output of outputs) requireNativeArtifact(output);
return {
outputs,
profileMatch: options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout),
};
}
/** Merges raw or indexed IR profiles into one indexed profile. */
export function mergeNativeProfiles(options: {
executable: string;
packageRoot: string;
compilerVersion: string;
target: NativeTargetSpec;
helper: NativeHelperSpec;
outputPath: string;
inputPaths: readonly string[];
sparse: boolean;
}): void {
verifyNativeHelper(options);
runNativeTool(options.executable, [
"merge-profiles",
"--output",
options.outputPath,
...(options.sparse ? ["--sparse"] : []),
...options.inputPaths,
]);
}
/** Helpers that answered `version` as expected: `known` is true when the
* helper file, as it is now, already did, and `remember` records that this
* one just did. */
export interface NativeHelperVerification {
known(): boolean;
remember(): void;
}
/** Verify installed identity even when a caller can reuse a cached object.
* With `verified`, the version probe runs only for a helper it does not
* know yet. */
export function verifyNativeHelper(options: {
executable: string;
packageRoot: string;
compilerVersion: string;
target: NativeTargetSpec;
helper: NativeHelperSpec;
verified?: NativeHelperVerification;
}): void {
const identity = JSON.parse(readFileSync(options.packageRoot + "/package.json", "utf8")) as {
name: string;
version: string;
};
if (
identity.name !== options.helper.packageName ||
identity.version !== options.compilerVersion
) {
throw new Error(
`LLVM helper package mismatch: expected ${options.helper.packageName}@${options.compilerVersion}`,
);
}
if (options.verified?.known() === true) return;
const version = JSON.parse(
runNativeTool(options.executable, ["version", "--format=json"]),
) as Record<string, unknown>;
validateNativeCodegenVersion(version, options.target, options.helper, options.compilerVersion);
options.verified?.remember();
}