mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Reuse linked executables on Linux (#2077)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
@@ -88,14 +89,19 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
write(family: string, key: string, bytes: string | Uint8Array): void {
|
||||
write(
|
||||
family: string,
|
||||
key: string,
|
||||
bytes: string | Uint8Array,
|
||||
digest = contentDigest(bytes),
|
||||
): void {
|
||||
let stage: string | null = null;
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
stage = mkdtempSync(join(directory, ".write-"));
|
||||
writeFileSync(join(stage, "payload"), bytes);
|
||||
writeFileSync(join(stage, "digest"), contentDigest(bytes) + "\n");
|
||||
writeFileSync(join(stage, "digest"), digest + "\n");
|
||||
const path = join(directory, key);
|
||||
// Concurrent writers publish identical content under the same key;
|
||||
// a reader between the two renames simply treats it as a miss.
|
||||
@@ -112,6 +118,29 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a marker or entry exists, without reading or checking it. */
|
||||
has(family: string, key: string): boolean {
|
||||
return existsSync(join(this.root, family, key));
|
||||
}
|
||||
|
||||
/** Records an empty marker that has() finds. */
|
||||
mark(family: string, key: string): void {
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(directory, key), "");
|
||||
} catch {
|
||||
/* Cache failures never change a build's result. */
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes `bytes` under their content digest, and returns the digest. */
|
||||
writeContent(family: string, bytes: Uint8Array): string {
|
||||
const digest = contentDigest(bytes);
|
||||
this.write(family, digest, bytes, digest);
|
||||
return digest;
|
||||
}
|
||||
|
||||
/** Publish text parts without joining them into one string. */
|
||||
writeParts(family: string, key: string, parts: Generator<string>): void {
|
||||
let stage: string | null = null;
|
||||
@@ -149,6 +178,7 @@ export class NativeCache {
|
||||
"object",
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"linked",
|
||||
"binary",
|
||||
"dsym",
|
||||
"coverage",
|
||||
|
||||
@@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js";
|
||||
import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { evaluateNativeComptime } from "./comptime.js";
|
||||
import { contentDigest, fileDigest, type NativeCache } from "./cache.js";
|
||||
import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cache.js";
|
||||
import {
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
@@ -168,6 +168,7 @@ export class NativeCompiler {
|
||||
outputKind: "obj" | "asm",
|
||||
partitions = 1,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest: () => string = () => fileDigest(input),
|
||||
): string[] {
|
||||
const toolchain = this.toolchain;
|
||||
const outputs = nativePartitionPaths(output, partitions);
|
||||
@@ -186,7 +187,7 @@ export class NativeCompiler {
|
||||
key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: fileDigest(input),
|
||||
llvm: llvmDigest(),
|
||||
outputKind,
|
||||
optimization,
|
||||
source,
|
||||
@@ -253,6 +254,7 @@ export class NativeCompiler {
|
||||
stage: string,
|
||||
library: boolean,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest?: () => string,
|
||||
): string[] {
|
||||
// Library archives merge dev shards relocatably; executables link the
|
||||
// helper's program partitions directly.
|
||||
@@ -263,7 +265,16 @@ export class NativeCompiler {
|
||||
const split = llvm === null ? null : splitLlvmLibraryProgram(llvm);
|
||||
if (split === null) {
|
||||
const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size);
|
||||
return this.emitObject(input, output, source, optimization, "obj", partitions, profile);
|
||||
return this.emitObject(
|
||||
input,
|
||||
output,
|
||||
source,
|
||||
optimization,
|
||||
"obj",
|
||||
partitions,
|
||||
profile,
|
||||
llvmDigest,
|
||||
);
|
||||
}
|
||||
const objects: string[] = [];
|
||||
for (const [index, shard] of split.shards.entries()) {
|
||||
@@ -358,6 +369,9 @@ export class NativeCompiler {
|
||||
return { ok: true, artifact: { kind: "ir", path: output } };
|
||||
}
|
||||
const { llvm, features } = prepared;
|
||||
// The executable and object caches key the same LLVM text.
|
||||
let llvmDigest: string | undefined;
|
||||
const digestLlvm = (): string => (llvmDigest ??= partsDigest(llvm));
|
||||
const inputDirectory = join(stage, "input");
|
||||
mkdirSync(inputDirectory);
|
||||
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
|
||||
@@ -385,7 +399,14 @@ export class NativeCompiler {
|
||||
const executableCache =
|
||||
executablePack === null
|
||||
? null
|
||||
: openNativeExecutableCache(this.cache, toolchain, llvm, options, ffi, executablePack);
|
||||
: openNativeExecutableCache(
|
||||
this.cache,
|
||||
toolchain,
|
||||
digestLlvm,
|
||||
options,
|
||||
ffi,
|
||||
executablePack,
|
||||
);
|
||||
const restored = executableCache?.restore(stagedOutput) ?? false;
|
||||
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
||||
if (!restored) {
|
||||
@@ -408,6 +429,7 @@ export class NativeCompiler {
|
||||
stage,
|
||||
false,
|
||||
profile,
|
||||
digestLlvm,
|
||||
);
|
||||
else
|
||||
this.emitObject(
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, expect, test } from "vitest";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import { NativeCache } from "./cache.js";
|
||||
import { openNativeExecutableCache } from "./executable-cache.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
const saved = new Map<string, string | undefined>();
|
||||
beforeEach(() => {
|
||||
for (const name of ["SCRIPTC_LINKER", "SCRIPTC_LLVM_HELPER", "CC", "LDFLAGS", "LIBRARY_PATH"]) {
|
||||
saved.set(name, process.env[name]);
|
||||
delete process.env[name];
|
||||
}
|
||||
});
|
||||
afterEach(() => {
|
||||
for (const [name, value] of saved) {
|
||||
if (value === undefined) delete process.env[name];
|
||||
else process.env[name] = value;
|
||||
}
|
||||
saved.clear();
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** A toolchain whose linker is a driver script named `driver`; it answers
|
||||
* `-print-prog-name=clang` with its own path. */
|
||||
function fixture(platform: string, driver: string) {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-executable-cache-open-"));
|
||||
directories.push(root);
|
||||
const bin = join(root, "bin");
|
||||
const helper = join(root, "helper");
|
||||
const runtime = join(root, "runtime");
|
||||
for (const directory of [bin, helper, runtime]) mkdirSync(directory);
|
||||
const linker = join(bin, driver);
|
||||
writeFileSync(linker, '#!/bin/sh\necho "$0"\n');
|
||||
chmodSync(linker, 0o755);
|
||||
writeFileSync(join(helper, "codegen"), "helper");
|
||||
writeFileSync(join(helper, "package.json"), "{}");
|
||||
writeFileSync(join(runtime, "package.json"), "{}");
|
||||
writeFileSync(join(runtime, "runtime-pack.json"), "{}");
|
||||
const toolchain = {
|
||||
compilerVersion: "test",
|
||||
target: { platform },
|
||||
helperExecutable: join(helper, "codegen"),
|
||||
helperPackageRoot: helper,
|
||||
runtimePackRoot: runtime,
|
||||
linker,
|
||||
linkerArgs: [],
|
||||
dsymutil: "dsymutil",
|
||||
} as unknown as NativeToolchain;
|
||||
const pack = {
|
||||
root: runtime,
|
||||
manifest: { licenses: [] },
|
||||
packageText: "{}",
|
||||
manifestText: "{}",
|
||||
selected: { runtime: [], archives: [] },
|
||||
flavor: "release",
|
||||
} as unknown as NativeRuntimeSelection;
|
||||
const open = () =>
|
||||
openNativeExecutableCache(
|
||||
new NativeCache(join(root, "cache")),
|
||||
toolchain,
|
||||
() => "llvm digest",
|
||||
{ outPath: join(root, "program"), outDir: root, optimization: "release" },
|
||||
null,
|
||||
pack,
|
||||
);
|
||||
return { open };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"Linux builds that link with a clang driver use the executable cache",
|
||||
() => {
|
||||
expect(fixture("linux", "clang").open()).not.toBeNull();
|
||||
expect(fixture("linux", "clang-22").open()).not.toBeNull();
|
||||
expect(fixture("linux", "gcc").open()).toBeNull();
|
||||
expect(fixture("windows", "clang").open()).toBeNull();
|
||||
expect(fixture("wasi", "clang").open()).toBeNull();
|
||||
// On darwin only Apple's driver at /usr/bin/clang qualifies.
|
||||
expect(fixture("darwin", "clang").open()).toBeNull();
|
||||
process.env["SCRIPTC_LLVM_HELPER"] = "/elsewhere/codegen";
|
||||
expect(fixture("linux", "clang").open()).toBeNull();
|
||||
},
|
||||
);
|
||||
@@ -222,3 +222,61 @@ test("a failed or empty link trace cannot publish a completed executable", () =>
|
||||
});
|
||||
expect(entry.trace([], f.stage)).toBe(false);
|
||||
});
|
||||
|
||||
test.each(["ld", "crt", "libc"] as const)(
|
||||
"a Linux key's second link records the linker, startup objects and libc; replacing the %s misses",
|
||||
(input) => {
|
||||
const f = fixture();
|
||||
const lib = join(f.root, "lib");
|
||||
mkdirSync(lib);
|
||||
const paths = {
|
||||
ld: join(f.root, "ld"),
|
||||
crt: join(lib, "Scrt1.o"),
|
||||
libc: join(lib, "libc.so.6"),
|
||||
};
|
||||
writeFileSync(paths.ld, "linker");
|
||||
writeFileSync(paths.crt, "startup object");
|
||||
writeFileSync(paths.libc, "c library");
|
||||
const nonshared = join(lib, "libc_nonshared.a");
|
||||
writeFileSync(nonshared, "archive");
|
||||
const program = join(f.stage, "program.o");
|
||||
writeFileSync(program, "program");
|
||||
// With -v, clang prints the linker job it runs, as -### would; GNU ld's
|
||||
// -t names each file it loads, and an archive member as archive(member).
|
||||
const job = ` "${paths.ld}" "-pie" "-o" "${f.output}" "${paths.crt}" "-L${lib}" "${program}" "-lc"`;
|
||||
const loaded = [paths.crt, program, paths.libc, `${nonshared}(atexit.oS)`].join("\n");
|
||||
vi.mocked(spawnSync).mockImplementation((_command, args) => {
|
||||
const verbose = (args as string[]).includes("-v");
|
||||
return {
|
||||
pid: 1,
|
||||
status: 0,
|
||||
signal: null,
|
||||
output: [],
|
||||
stdout: verbose ? loaded + "\n" : "",
|
||||
stderr: verbose ? job + "\n" : "",
|
||||
};
|
||||
});
|
||||
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
|
||||
// A key's first link is not traced; the caller links the output itself.
|
||||
expect(open().trace(["-o", f.output], f.stage)).toBe(false);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
// Its next link traces both the driver and the linker in one run.
|
||||
const entry = open();
|
||||
expect(entry.trace(["-o", f.output], f.stage)).toBe(true);
|
||||
expect(spawnSync).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(spawnSync).mock.calls[0]![1]).toEqual(["-o", f.output, "-v", "-Wl,-t"]);
|
||||
entry.publish(f.output);
|
||||
const recorded = (
|
||||
JSON.parse(f.cache.read("executable", f.key)!.toString()) as { inputs: { path: string }[] }
|
||||
).inputs.map((saved) => saved.path);
|
||||
expect(recorded).toEqual(expect.arrayContaining([...Object.values(paths), nonshared, lib]));
|
||||
expect(recorded).not.toContain(program);
|
||||
expect(open().restore(f.destination)).toBe(true);
|
||||
const before = statSync(paths[input]);
|
||||
writeFileSync(paths[input] + ".new", "x".repeat(before.size));
|
||||
utimesSync(paths[input] + ".new", before.atime, before.mtime);
|
||||
renameSync(paths[input] + ".new", paths[input]);
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { runNativeTool } from "../backend/native-tools.js";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { contentDigest, NativeCache, partsDigest } from "./cache.js";
|
||||
import { contentDigest, NativeCache } from "./cache.js";
|
||||
|
||||
interface InputIdentity {
|
||||
path: string;
|
||||
@@ -48,9 +48,22 @@ function identity(path: string): InputIdentity {
|
||||
};
|
||||
}
|
||||
|
||||
function sameIdentity(a: InputIdentity, b: InputIdentity): boolean {
|
||||
return (
|
||||
a.path === b.path &&
|
||||
a.canonical === b.canonical &&
|
||||
a.kind === b.kind &&
|
||||
a.dev === b.dev &&
|
||||
a.ino === b.ino &&
|
||||
a.size === b.size &&
|
||||
a.mtime === b.mtime &&
|
||||
a.ctime === b.ctime
|
||||
);
|
||||
}
|
||||
|
||||
function stillMatches(inputs: InputIdentity[]): boolean {
|
||||
try {
|
||||
return inputs.every((input) => JSON.stringify(identity(input.path)) === JSON.stringify(input));
|
||||
return inputs.every((input) => sameIdentity(identity(input.path), input));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -165,6 +178,8 @@ function stageSymbols(bytes: Buffer, binary: string): void {
|
||||
export class NativeExecutableCache {
|
||||
private inputs: InputIdentity[];
|
||||
private traced = false;
|
||||
/** Linux: no entry exists and the key was never linked before. */
|
||||
private readonly firstLink: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly cache: NativeCache,
|
||||
@@ -172,23 +187,28 @@ export class NativeExecutableCache {
|
||||
private readonly linker: string,
|
||||
private readonly debug: boolean,
|
||||
paths: string[],
|
||||
/** Linux: trace with one verbose link, from a key's second link on. */
|
||||
private readonly linux = false,
|
||||
) {
|
||||
this.inputs = [...new Set(paths)].sort().map(identity);
|
||||
this.firstLink = linux && !cache.has("executable", key) && !cache.has("linked", key);
|
||||
// A first link neither restores nor traces, so it needs no identities.
|
||||
this.inputs = this.firstLink ? [] : [...new Set(paths)].sort().map(identity);
|
||||
}
|
||||
|
||||
restore(output: string): boolean {
|
||||
if (this.firstLink) return false;
|
||||
try {
|
||||
const metadata = this.cache.read("executable", this.key);
|
||||
if (metadata === null) return false;
|
||||
const entry: unknown = JSON.parse(metadata.toString("utf8"));
|
||||
if (!validEntry(entry) || !stillMatches(entry.inputs) || !stillMatches(this.inputs))
|
||||
return false;
|
||||
const saved = new Map(entry.inputs.map((input) => [input.path, input]));
|
||||
if (
|
||||
!this.inputs.every((input) =>
|
||||
entry.inputs.some(
|
||||
(saved) => saved.path === input.path && JSON.stringify(saved) === JSON.stringify(input),
|
||||
),
|
||||
)
|
||||
!this.inputs.every((input) => {
|
||||
const recorded = saved.get(input.path);
|
||||
return recorded !== undefined && sameIdentity(recorded, input);
|
||||
})
|
||||
)
|
||||
return false;
|
||||
if (this.debug !== (entry.symbols !== null)) return false;
|
||||
@@ -209,14 +229,24 @@ export class NativeExecutableCache {
|
||||
* Directories from the driver's search line detect a newly added candidate.
|
||||
* Private build inputs already belong to the LLVM/runtime cache identity.
|
||||
* A true result means the requested output was linked successfully and its
|
||||
* inputs remained stable. The caller can publish that output directly. */
|
||||
* inputs remained stable. The caller can publish that output directly. On
|
||||
* false, the caller links the output itself. */
|
||||
trace(args: string[], stage: string): boolean {
|
||||
this.traced = false;
|
||||
try {
|
||||
// On Linux a key's first link only records that it was linked; a later
|
||||
// link of the same key traces, so a build that runs once traces nothing.
|
||||
if (this.firstLink) {
|
||||
this.cache.mark("linked", this.key);
|
||||
return false;
|
||||
}
|
||||
const roots = [resolve(stage), realpathSync(stage)];
|
||||
const dry = toolOutput(this.linker, [...args, "-###"]);
|
||||
const driverInputs = tracePaths(dry, true, roots).map(identity);
|
||||
const linked = toolOutput(this.linker, [...args, "-Wl,-t"]);
|
||||
let driverInputs = this.linux
|
||||
? []
|
||||
: tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity);
|
||||
// On Linux, -v prints the jobs that -### would, while the link runs.
|
||||
const linked = toolOutput(this.linker, [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]);
|
||||
if (this.linux) driverInputs = tracePaths(linked, true, roots).map(identity);
|
||||
const paths = tracePaths(linked, false, roots);
|
||||
if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs))
|
||||
return false;
|
||||
@@ -233,10 +263,8 @@ export class NativeExecutableCache {
|
||||
if (!this.traced || !stillMatches(this.inputs)) return;
|
||||
const binary = readFileSync(output);
|
||||
const symbols = this.debug ? readSymbols(output) : null;
|
||||
const binaryKey = contentDigest(binary);
|
||||
const symbolsKey = symbols === null ? null : contentDigest(symbols);
|
||||
this.cache.write("binary", binaryKey, binary);
|
||||
if (symbols !== null && symbolsKey !== null) this.cache.write("dsym", symbolsKey, symbols);
|
||||
const binaryKey = this.cache.writeContent("binary", binary);
|
||||
const symbolsKey = symbols === null ? null : this.cache.writeContent("dsym", symbols);
|
||||
if (!stillMatches(this.inputs)) return;
|
||||
const entry: ExecutableEntry = {
|
||||
schema: "scriptc.native-executable.v1",
|
||||
@@ -254,16 +282,18 @@ export class NativeExecutableCache {
|
||||
export function openNativeExecutableCache(
|
||||
cache: NativeCache | null,
|
||||
toolchain: NativeToolchain,
|
||||
llvm: readonly string[],
|
||||
llvmDigest: () => string,
|
||||
options: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
pack: NativeRuntimeSelection,
|
||||
): NativeExecutableCache | null {
|
||||
// Match the established complete-cache contract: default Apple driver,
|
||||
// known runtime inputs, and no wrapper or caller-supplied library searches.
|
||||
// Match the established complete-cache contract: the default Apple driver
|
||||
// or a Linux clang driver, known runtime inputs, and no wrapper or
|
||||
// caller-supplied library searches.
|
||||
const platform = toolchain.target.platform;
|
||||
if (
|
||||
cache === null ||
|
||||
toolchain.target.platform !== "darwin" ||
|
||||
(platform !== "darwin" && platform !== "linux") ||
|
||||
options.sanitize ||
|
||||
// The key names a profile by path, not by its bytes.
|
||||
options.profileGenerate !== undefined ||
|
||||
@@ -277,9 +307,23 @@ export function openNativeExecutableCache(
|
||||
return null;
|
||||
try {
|
||||
const linker = commandPath(toolchain.linker);
|
||||
if (realpathSync(linker) !== "/usr/bin/clang") return null;
|
||||
const effective = commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim());
|
||||
const debug = options.optimization === "dev" && !options.strip;
|
||||
// Apple's driver at /usr/bin/clang, or a clang driver on Linux. Both
|
||||
// name every input of a link in their -### and -Wl,-t traces.
|
||||
if (
|
||||
platform === "darwin"
|
||||
? realpathSync(linker) !== "/usr/bin/clang"
|
||||
: !/^clang(-\d+)?$/.test(basename(realpathSync(linker)))
|
||||
)
|
||||
return null;
|
||||
// Apple's /usr/bin/clang is a shim for the active developer directory's
|
||||
// driver; a Linux clang driver is itself the driver.
|
||||
const effective =
|
||||
platform === "darwin"
|
||||
? commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim())
|
||||
: linker;
|
||||
// Linux keeps debug information in the executable; darwin keeps it in a
|
||||
// separate dSYM, which the cache stores beside the executable.
|
||||
const debug = platform === "darwin" && options.optimization === "dev" && !options.strip;
|
||||
const paths = [
|
||||
linker,
|
||||
effective,
|
||||
@@ -291,6 +335,7 @@ export function openNativeExecutableCache(
|
||||
(artifact) => join(pack.root, artifact.path),
|
||||
),
|
||||
];
|
||||
if (platform === "linux") paths.push(join(pack.root, "runtime-pack.json"));
|
||||
if (debug) {
|
||||
const dsymutil = commandPath(toolchain.dsymutil);
|
||||
if (realpathSync(dsymutil) !== "/usr/bin/dsymutil") return null;
|
||||
@@ -299,11 +344,16 @@ export function openNativeExecutableCache(
|
||||
const key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: partsDigest(llvm),
|
||||
llvm: llvmDigest(),
|
||||
options,
|
||||
toolchain,
|
||||
runtimeIdentity: pack.packageText,
|
||||
runtimeManifest: pack.manifestText,
|
||||
// On Linux the manifest's identity names it: restore checks that
|
||||
// identity again, as it does every other input.
|
||||
runtimeManifest:
|
||||
platform === "darwin"
|
||||
? pack.manifestText
|
||||
: identity(join(pack.root, "runtime-pack.json")),
|
||||
linker: identity(linker),
|
||||
effective: identity(effective),
|
||||
environment: toolchainEnvironmentFingerprint(),
|
||||
@@ -311,7 +361,7 @@ export function openNativeExecutableCache(
|
||||
path: process.env["PATH"] ?? null,
|
||||
}),
|
||||
);
|
||||
return new NativeExecutableCache(cache, key, linker, debug, paths);
|
||||
return new NativeExecutableCache(cache, key, linker, debug, paths, platform === "linux");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user