mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
929 lines
34 KiB
TypeScript
929 lines
34 KiB
TypeScript
import type { NativeOptimization } from "../backend/optimization.js";
|
|
import { compilationTiming } from "../timing.js";
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
renameSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { basename, dirname, join, resolve } from "node:path";
|
|
import { writeTextPartsSync } from "./text-file.js";
|
|
import type {
|
|
AnalyzeOptions,
|
|
AnalyzeResult,
|
|
CompileFailure,
|
|
CompileLibraryOptions,
|
|
CompileLibraryResult,
|
|
CompileRequestOptions,
|
|
CompileRequestResult,
|
|
} from "../compile-types.js";
|
|
import { LlvmUnsupportedError } from "../backend/llvm/emitter.js";
|
|
import { executableLinkInputs, systemLibraryLinkerArgs } from "../backend/link-plan-core.js";
|
|
import { formatNativeLinkInfo } from "../backend/native-link-info-core.js";
|
|
import {
|
|
emitNativeObject,
|
|
requireNativeArtifact,
|
|
runNativeTool,
|
|
verifyNativeHelper,
|
|
type NativeHelperVerification,
|
|
} from "../backend/native-tools.js";
|
|
import {
|
|
type NativeRuntimeSelection,
|
|
selectNativeRuntimePack,
|
|
runtimePackManifest,
|
|
stageNativeRuntimeSelection,
|
|
type RuntimePackStore,
|
|
} from "../backend/runtime-pack-native.js";
|
|
import { RuntimePackError } from "../backend/runtime-pack-core.js";
|
|
import {
|
|
NativeCodegenError,
|
|
nativeCodegenParts,
|
|
nativePartitionPaths,
|
|
nativeProgramPartitions,
|
|
} from "../backend/native-codegen-core.js";
|
|
import { llvmRefusalDiag } from "../backend/target-diagnostics.js";
|
|
import { nativeCodegenDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
|
import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js";
|
|
import { analyzeWithFrontend } from "../frontend/analysis.js";
|
|
import { analyzeWithVerdictCache } from "../coverage/verdict-cache.js";
|
|
import type { FrontendFactory } from "../frontend/pipeline.js";
|
|
import { runNativeFrontend } from "../frontend/pipeline-native.js";
|
|
import { loadLibraryProfile } from "../library/library-profile.js";
|
|
import {
|
|
libraryLocalizeSymbols,
|
|
libraryWasmExports,
|
|
libraryWasmRefusal,
|
|
} from "../library/prepare.js";
|
|
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, partsDigest, type NativeCache } from "./cache.js";
|
|
import {
|
|
nativeProfileKey,
|
|
nativeProfileLayout,
|
|
nativeProfileRefusal,
|
|
parseNativeProfileMatch,
|
|
profileHasContextCounts,
|
|
type NativeProfileMatch,
|
|
type NativeProfileOptions,
|
|
} from "../backend/native-profile.js";
|
|
import { splitLlvmLibraryProgram } from "../backend/llvm/split.js";
|
|
import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js";
|
|
import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js";
|
|
import { inputsWithoutLld, lldLinkerArgs } from "../backend/lld.js";
|
|
import {
|
|
hugeTextLinkerArgs,
|
|
hugeTextProgram,
|
|
rewriteHugeTextExecutable,
|
|
} from "../backend/huge-text.js";
|
|
import { openNativeExecutableCache } from "./executable-cache.js";
|
|
import { LinkerJobs } from "./linker-job.js";
|
|
import { nativeFileIdentity } from "./file-identity.js";
|
|
import {
|
|
staticExecutableRefusal,
|
|
staticLinkFailureNote,
|
|
staticLinkMapArgs,
|
|
staticLinkRefusal,
|
|
} from "../backend/targets.js";
|
|
|
|
/** The first partition keeps the whole-object key used before partitioning. */
|
|
function partitionCacheKey(key: string, index: number): string {
|
|
return index === 0 ? key : contentDigest(`${key}:${index}`);
|
|
}
|
|
|
|
/** The object cache entry that holds a cached program's profile match. */
|
|
function profileMatchCacheKey(key: string): string {
|
|
return contentDigest(`${key}:profile-match`);
|
|
}
|
|
|
|
/** A program's native objects, and how a --profile-use build's profile
|
|
* matched its functions. */
|
|
interface NativeObjects {
|
|
paths: string[];
|
|
profileMatch?: NativeProfileMatch;
|
|
}
|
|
|
|
function failure(error: unknown, entry: string, sources: Map<string, string>): CompileFailure {
|
|
return {
|
|
ok: false,
|
|
diagnostics: [
|
|
nativeCodegenDiag("SC3004", error instanceof Error ? error.message : String(error), entry),
|
|
],
|
|
sourceTexts: sources,
|
|
};
|
|
}
|
|
|
|
/** The request's profile. An indexed profile is read here so every cache keys
|
|
* its bytes. */
|
|
function nativeProfile(options: CompileRequestOptions): NativeProfileOptions | undefined {
|
|
const generate = options.profileGenerate;
|
|
if (options.profileUse === undefined) return generate === undefined ? undefined : { generate };
|
|
const path = resolve(options.profileUse);
|
|
try {
|
|
const bytes = readFileSync(path);
|
|
const use = {
|
|
path,
|
|
digest: contentDigest(bytes),
|
|
contextSensitive: profileHasContextCounts(bytes),
|
|
};
|
|
return generate === undefined ? { use } : { generate, use };
|
|
} catch (error) {
|
|
throw new NativeCodegenError(
|
|
"SC3002",
|
|
`cannot read --profile-use file ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Shared frontend semantics with native processes for code generation and linking. */
|
|
export class NativeCompiler {
|
|
private readonly frontend: FrontendFactory;
|
|
|
|
constructor(
|
|
readonly toolchain: NativeToolchain,
|
|
private readonly cache: NativeCache | null = null,
|
|
) {
|
|
const evaluator = toolchain.comptimeExecutable;
|
|
this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic, options) =>
|
|
runNativeFrontend(
|
|
entry,
|
|
toolchain.ts7Executable,
|
|
npmStatic,
|
|
externalTypes,
|
|
evaluator === undefined
|
|
? undefined
|
|
: (source, timeout) =>
|
|
evaluateNativeComptime(source, timeout, toolchain.ts7Executable, evaluator),
|
|
libraryNpmStatic,
|
|
options,
|
|
);
|
|
}
|
|
|
|
analyze(entry: string, options: AnalyzeOptions): AnalyzeResult {
|
|
return analyzeWithFrontend(
|
|
resolve(entry),
|
|
options,
|
|
this.toolchain.target.platform,
|
|
this.frontend,
|
|
);
|
|
}
|
|
|
|
/** analyze() behind the coverage verdict cache: an unchanged program
|
|
* replays the previous verdict (exact probe replay; comment-only edits
|
|
* relower here, since the token comparison is the Node frontend's). */
|
|
analyzeCached(entry: string, options: AnalyzeOptions): AnalyzeResult {
|
|
entry = resolve(entry);
|
|
const cache = this.cache;
|
|
if (cache === null) return this.analyze(entry, options);
|
|
let identity: string;
|
|
try {
|
|
identity = `${this.toolchain.compilerVersion}\0${nativeFileIdentity(process.execPath)}`;
|
|
} catch {
|
|
return this.analyze(entry, options);
|
|
}
|
|
const { result } = analyzeWithVerdictCache(
|
|
entry,
|
|
options,
|
|
{
|
|
store: {
|
|
read: (key) => cache.read("coverage", key)?.toString("utf8") ?? null,
|
|
write: (key, text) => cache.write("coverage", key, text),
|
|
},
|
|
identity,
|
|
platform: this.toolchain.target.platform,
|
|
},
|
|
() => this.analyze(entry, options),
|
|
);
|
|
return result;
|
|
}
|
|
|
|
/** Runtime pack selections and verified artifact identities, kept in the
|
|
* cache. */
|
|
private runtimePackStore(): RuntimePackStore | undefined {
|
|
const cache = this.cache;
|
|
if (cache === null) return undefined;
|
|
return {
|
|
read: (key) => cache.read("runtime-pack", key)?.toString("utf8") ?? null,
|
|
write: (key, text) => cache.write("runtime-pack", key, text),
|
|
};
|
|
}
|
|
|
|
/** The helpers whose `version` answer this cache recorded, by file identity:
|
|
* the probe runs again only when the helper file changes. */
|
|
private helperVerification(): { verified?: NativeHelperVerification } {
|
|
const cache = this.cache;
|
|
const toolchain = this.toolchain;
|
|
if (cache === null) return {};
|
|
let identity: string;
|
|
let key: string;
|
|
try {
|
|
identity = nativeFileIdentity(toolchain.helperExecutable);
|
|
key = contentDigest(
|
|
JSON.stringify({
|
|
schema: 1,
|
|
helper: identity,
|
|
compiler: nativeFileIdentity(process.execPath),
|
|
version: toolchain.compilerVersion,
|
|
target: toolchain.target.name,
|
|
package: toolchain.helper.packageName,
|
|
}),
|
|
);
|
|
} catch {
|
|
return {};
|
|
}
|
|
return {
|
|
verified: {
|
|
known: () => cache.read("helper", key) !== null,
|
|
remember: () => {
|
|
try {
|
|
if (nativeFileIdentity(toolchain.helperExecutable) === identity)
|
|
cache.write("helper", key, "verified\n");
|
|
} catch {
|
|
/* A failed write only means probing again next time. */
|
|
}
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private emitObject(
|
|
input: string,
|
|
output: string,
|
|
source: string,
|
|
optimization: NativeOptimization,
|
|
outputKind: "obj" | "asm",
|
|
partitions = 1,
|
|
profile?: NativeProfileOptions,
|
|
llvmDigest: () => string = () => fileDigest(input),
|
|
splitCodegen = false,
|
|
): NativeObjects {
|
|
const toolchain = this.toolchain;
|
|
const outputs = nativePartitionPaths(output, partitions);
|
|
const helperOptions = {
|
|
executable: toolchain.helperExecutable,
|
|
packageRoot: toolchain.helperPackageRoot,
|
|
compilerVersion: toolchain.compilerVersion,
|
|
target: toolchain.target,
|
|
helper: toolchain.helper,
|
|
...this.helperVerification(),
|
|
};
|
|
let key: string | null = null;
|
|
let identity: string | null = null;
|
|
if (this.cache !== null && process.env["SCRIPTC_LLVM_HELPER"] === undefined) {
|
|
try {
|
|
identity = nativeFileIdentity(toolchain.helperExecutable);
|
|
key = contentDigest(
|
|
JSON.stringify({
|
|
schema: 1,
|
|
llvm: llvmDigest(),
|
|
outputKind,
|
|
optimization,
|
|
source,
|
|
target: toolchain.target,
|
|
helper: identity,
|
|
identity: readFileSync(join(toolchain.helperPackageRoot, "package.json"), "utf8"),
|
|
version: toolchain.compilerVersion,
|
|
...(partitions > 1 ? { partitions } : {}),
|
|
...(splitCodegen ? { split: "codegen" } : {}),
|
|
...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }),
|
|
}),
|
|
);
|
|
const cached = outputs.map((_, index) =>
|
|
this.cache!.read("object", partitionCacheKey(key!, index)),
|
|
);
|
|
if (cached.every((bytes) => bytes !== null)) {
|
|
verifyNativeHelper(helperOptions);
|
|
if (nativeFileIdentity(toolchain.helperExecutable) === identity) {
|
|
cached.forEach((bytes, index) => writeFileSync(outputs[index]!, bytes!));
|
|
if (profile?.use === undefined) return { paths: outputs };
|
|
// Without the cached match, every partition's counts are unknown.
|
|
const match = this.cache.read("object", profileMatchCacheKey(key));
|
|
return {
|
|
paths: outputs,
|
|
profileMatch: (match === null
|
|
? null
|
|
: parseNativeProfileMatch(match.toString("utf8"))) ?? {
|
|
annotated: 0,
|
|
mismatched: 0,
|
|
missing: 0,
|
|
unknownPartitions: outputs.length,
|
|
},
|
|
};
|
|
}
|
|
key = null;
|
|
}
|
|
} catch {
|
|
key = null;
|
|
}
|
|
}
|
|
const partitionCache =
|
|
this.cache === null || identity === null || partitions < 2 || splitCodegen
|
|
? null
|
|
: this.cache.partitionDirectory(identity);
|
|
const { profileMatch } = emitNativeObject({
|
|
...helperOptions,
|
|
inputPath: input,
|
|
outputPath: output,
|
|
sourcePath: source,
|
|
optimization,
|
|
outputKind,
|
|
partitions,
|
|
...(splitCodegen ? { splitCodegen } : {}),
|
|
...(partitionCache === null ? {} : { partitionCache }),
|
|
...(profile === undefined ? {} : { profile }),
|
|
});
|
|
if (this.cache !== null && key !== null) {
|
|
try {
|
|
if (nativeFileIdentity(toolchain.helperExecutable) === identity) {
|
|
// The match goes in first: a reader that finds every partition finds it too.
|
|
if (profileMatch !== null)
|
|
this.cache.write(
|
|
"object",
|
|
profileMatchCacheKey(key),
|
|
`${JSON.stringify({
|
|
profile: {
|
|
annotated: profileMatch.annotated,
|
|
mismatched: profileMatch.mismatched,
|
|
missing: profileMatch.missing,
|
|
unknown_partitions: profileMatch.unknownPartitions,
|
|
},
|
|
})}\n`,
|
|
);
|
|
outputs.forEach((path, index) =>
|
|
this.cache!.write("object", partitionCacheKey(key!, index), readFileSync(path)),
|
|
);
|
|
}
|
|
} catch {
|
|
/* An updated helper or unavailable cache only prevents reuse. */
|
|
}
|
|
}
|
|
return profileMatch === null ? { paths: outputs } : { paths: outputs, profileMatch };
|
|
}
|
|
|
|
private emitProgramObject(
|
|
input: string,
|
|
output: string,
|
|
source: string,
|
|
optimization: NativeOptimization,
|
|
stage: string,
|
|
library: boolean,
|
|
profile?: NativeProfileOptions,
|
|
llvmDigest?: () => string,
|
|
): NativeObjects {
|
|
// Library archives merge dev shards relocatably; executables link the
|
|
// helper's program partitions directly.
|
|
const llvm =
|
|
library && optimization === "dev" && this.toolchain.target.platform !== "wasi"
|
|
? readFileSync(input, "utf8")
|
|
: null;
|
|
const split = llvm === null ? null : splitLlvmLibraryProgram(llvm);
|
|
if (split === null) {
|
|
const size = statSync(input).size;
|
|
// An optimized program that stays one partition is optimized whole and
|
|
// generates code for its parts concurrently.
|
|
const codegenParts = library || optimization === "dev" ? 1 : nativeCodegenParts(size);
|
|
const partitions = library ? 1 : Math.max(nativeProgramPartitions(size), codegenParts);
|
|
return this.emitObject(
|
|
input,
|
|
output,
|
|
source,
|
|
optimization,
|
|
"obj",
|
|
partitions,
|
|
profile,
|
|
llvmDigest,
|
|
codegenParts > 1,
|
|
);
|
|
}
|
|
const objects: string[] = [];
|
|
for (const [index, shard] of split.shards.entries()) {
|
|
const shardInput = join(stage, `shard-${index}.ll`);
|
|
const shardOutput = join(stage, `shard-${index}` + this.toolchain.target.outputSuffixes.obj);
|
|
writeFileSync(shardInput, shard.source());
|
|
this.emitObject(shardInput, shardOutput, source + "." + shard.name, optimization, "obj");
|
|
objects.push(shardOutput);
|
|
}
|
|
const merged = localizeNativeLibrary(this.toolchain, stage, objects, [], split.publicSymbols);
|
|
renameSync(merged, output);
|
|
return { paths: [output] };
|
|
}
|
|
|
|
compile(entry: string, options: CompileRequestOptions): CompileRequestResult {
|
|
const sink: { warnings: ScrDiagnostic[]; sources: Map<string, string> } = {
|
|
warnings: [],
|
|
sources: new Map(),
|
|
};
|
|
const result = this.compileWithWarnings(entry, options, sink);
|
|
// Divergence warnings ride a successful result (never a failure).
|
|
if (!result.ok || sink.warnings.length === 0) return result;
|
|
return { ...result, warnings: sink.warnings, sourceTexts: sink.sources };
|
|
}
|
|
|
|
private compileWithWarnings(
|
|
entry: string,
|
|
options: CompileRequestOptions,
|
|
sink: { warnings: ScrDiagnostic[]; sources: Map<string, string> },
|
|
): CompileRequestResult {
|
|
const timing = compilationTiming();
|
|
const toolchain = this.toolchain;
|
|
const target = toolchain.target;
|
|
const output = resolve(options.outPath);
|
|
const outputKind = options.outputKind ?? "exe";
|
|
const optimization = options.optimization ?? "release";
|
|
entry = resolve(entry);
|
|
let sourceTexts = new Map<string, string>();
|
|
let stage: string | null = null;
|
|
try {
|
|
if (entry === output) throw new Error("output path must differ from the entry source");
|
|
if (outputKind === "exe" && !target.supports.exe)
|
|
throw new NativeCodegenError(
|
|
"SC3002",
|
|
`${target.name} supports library archives, not executables`,
|
|
);
|
|
if (options.sanitize && (outputKind === "asm" || outputKind === "obj")) {
|
|
throw new NativeCodegenError(
|
|
"SC3002",
|
|
`--sanitize is not supported with --emit=${outputKind}; AddressSanitizer instrumentation parity is not available in the LLVM native helper yet`,
|
|
);
|
|
}
|
|
if (options.sanitize) sanitizerDriver(toolchain);
|
|
const profileRefusal = nativeProfileRefusal({
|
|
...options,
|
|
outputKind,
|
|
optimization,
|
|
sanitize: options.sanitize ?? false,
|
|
platform: target.platform,
|
|
});
|
|
if (profileRefusal !== null) throw new NativeCodegenError("SC3002", profileRefusal);
|
|
const staticRefusal = staticLinkRefusal({
|
|
staticLink: options.staticLink,
|
|
outputKind,
|
|
sanitize: options.sanitize ?? false,
|
|
target,
|
|
});
|
|
if (staticRefusal !== null) throw new NativeCodegenError("SC3002", staticRefusal);
|
|
const profile = nativeProfile(options);
|
|
let ffi: FfiProfile | null = null;
|
|
if (options.ffiProfilePath !== undefined) {
|
|
const loaded = loadFfiProfile(resolve(options.ffiProfilePath));
|
|
if (!loaded.ok) return { ok: false, diagnostics: loaded.diagnostics, sourceTexts };
|
|
ffi = loaded.profile;
|
|
}
|
|
const prepared = prepareNativeExecutable(
|
|
entry,
|
|
options,
|
|
ffi,
|
|
toolchain,
|
|
this.frontend,
|
|
this.cache,
|
|
timing,
|
|
);
|
|
if (!prepared.ok) return prepared;
|
|
sourceTexts = new Map(prepared.sources);
|
|
if (prepared.warnings !== undefined && prepared.warnings.length > 0) {
|
|
sink.warnings = prepared.warnings;
|
|
sink.sources = sourceTexts;
|
|
}
|
|
mkdirSync(dirname(output), { recursive: true });
|
|
stage = mkdtempSync(join(dirname(output), ".scriptc-native-"));
|
|
const stagedOutput = join(stage, basename(output));
|
|
const stem = basename(entry).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
|
const llvmPath = join(options.outDir, `${stem}.ll`);
|
|
if (outputKind === "ir") {
|
|
writeTextPartsSync(stagedOutput, prepared.ir!);
|
|
renameSync(stagedOutput, output);
|
|
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");
|
|
// In slices: the module text is never encoded whole.
|
|
writeTextPartsSync(llvmInput, llvm);
|
|
timing("llvm-write");
|
|
if (outputKind === "llvm") {
|
|
renameSync(stagedOutput, output);
|
|
return { ok: true, artifact: { kind: "llvm", path: output } };
|
|
}
|
|
const object =
|
|
outputKind === "exe"
|
|
? join(inputDirectory, "program" + target.outputSuffixes.obj)
|
|
: stagedOutput;
|
|
let executablePack: NativeRuntimeSelection | null = null;
|
|
const selectPack = (): NativeRuntimeSelection =>
|
|
(executablePack ??= selectNativeRuntimePack(
|
|
toolchain.runtimePackRoot,
|
|
target,
|
|
toolchain.compilerVersion,
|
|
features,
|
|
optimization,
|
|
"executable",
|
|
this.runtimePackStore(),
|
|
));
|
|
const executableCache =
|
|
outputKind === "exe"
|
|
? openNativeExecutableCache(this.cache, toolchain, digestLlvm, options, ffi, {
|
|
root: toolchain.runtimePackRoot,
|
|
features,
|
|
flavor: optimization,
|
|
select: selectPack,
|
|
})
|
|
: null;
|
|
// An unchanged rebuild keeps the executable the last build left.
|
|
const kept = executableCache?.keep(output) ?? false;
|
|
const restored = kept || (executableCache?.restore(stagedOutput) ?? false);
|
|
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
|
// The executable cache leaves out --profile-use builds, so this is set
|
|
// whenever the build has a profile to report.
|
|
let profileMatch: NativeProfileMatch | undefined;
|
|
if (!restored) {
|
|
// A missing or invalid runtime pack fails before code generation.
|
|
if (outputKind === "exe") selectPack();
|
|
let programObjects = [object];
|
|
if (options.sanitize) {
|
|
runNativeTool(sanitizerDriver(toolchain), [
|
|
...sanitizerFlags(toolchain, optimization),
|
|
"-c",
|
|
llvmInput,
|
|
"-o",
|
|
object,
|
|
]);
|
|
requireNativeArtifact(object);
|
|
} else if (outputKind === "exe") {
|
|
const objects = this.emitProgramObject(
|
|
llvmInput,
|
|
object,
|
|
entry,
|
|
optimization,
|
|
stage,
|
|
false,
|
|
profile,
|
|
digestLlvm,
|
|
);
|
|
programObjects = objects.paths;
|
|
profileMatch = objects.profileMatch;
|
|
} else
|
|
this.emitObject(
|
|
llvmInput,
|
|
object,
|
|
entry,
|
|
optimization,
|
|
outputKind === "asm" ? "asm" : "obj",
|
|
);
|
|
timing("native-object");
|
|
if (outputKind === "asm" || outputKind === "obj") {
|
|
const pack =
|
|
outputKind === "obj" && options.nativeLinkInfo
|
|
? selectNativeRuntimePack(
|
|
toolchain.runtimePackRoot,
|
|
target,
|
|
toolchain.compilerVersion,
|
|
features,
|
|
optimization,
|
|
"executable",
|
|
this.runtimePackStore(),
|
|
)
|
|
: null;
|
|
if (pack !== null)
|
|
stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore());
|
|
const info =
|
|
pack === null
|
|
? undefined
|
|
: formatNativeLinkInfo(
|
|
{ programObject: output, target, ffi },
|
|
toolchain.compilerVersion,
|
|
{
|
|
root: pack.root,
|
|
manifest: runtimePackManifest(pack),
|
|
selected: pack.selected,
|
|
flavor: pack.flavor,
|
|
},
|
|
);
|
|
renameSync(stagedOutput, output);
|
|
return {
|
|
ok: true,
|
|
artifact: {
|
|
kind: outputKind,
|
|
path: output,
|
|
...(info === undefined ? {} : { nativeLinkInfo: info }),
|
|
},
|
|
};
|
|
}
|
|
const pack = selectPack();
|
|
const runtime = options.sanitize
|
|
? buildSanitizedRuntime(toolchain, pack, join(stage, "runtime"), "executable", this.cache)
|
|
: stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore());
|
|
timing("runtime-stage");
|
|
const plan = executableLinkInputs({
|
|
target,
|
|
programObject: object,
|
|
programPartitions: programObjects.slice(1),
|
|
ffiLibraries: ffi?.libraries ?? [],
|
|
ffiSystemLibraries: ffi?.systemLibraries ?? [],
|
|
ffiFrameworks: ffi?.frameworks ?? [],
|
|
runtimeObjects: runtime.runtimeObjects,
|
|
runtimeArchives: runtime.archives,
|
|
runtimeSystemLibraries: runtime.systemLibraries,
|
|
optimization,
|
|
strip: options.strip ?? false,
|
|
staticLink: options.staticLink ?? false,
|
|
...(options.windowsSubsystem === undefined
|
|
? {}
|
|
: { windowsSubsystem: options.windowsSubsystem }),
|
|
...(profile?.generate === undefined ? {} : { profileGenerate: true }),
|
|
...(nativeProfileLayout(profile) ? { profileLayout: true } : {}),
|
|
});
|
|
// Linux links with lld when the driver finds it.
|
|
const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker);
|
|
const hugeText =
|
|
!options.sanitize && hugeTextProgram(target, [object, ...programObjects.slice(1)]);
|
|
const zig = process.env["SCRIPTC_LINKER"] === undefined && target.defaultLinker === "zig";
|
|
const linkMap = join(stage, "link.map");
|
|
const mapArgs = staticLinkMapArgs(target, options.staticLink === true, linkMap);
|
|
const linkArgsWith = (withLld: boolean): string[] => [
|
|
...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs),
|
|
...(withLld ? lld : []),
|
|
...hugeTextLinkerArgs(hugeText, (withLld && lld.length !== 0) || zig),
|
|
...plan.driverFlags,
|
|
...mapArgs,
|
|
...(withLld ? plan.inputs : inputsWithoutLld(target.platform, plan.inputs)),
|
|
...systemLibraryLinkerArgs(target, plan.systemLibraries),
|
|
"-o",
|
|
stagedOutput,
|
|
];
|
|
const linkArgs = linkArgsWith(lld.length !== 0);
|
|
// Host Linux links with lld run the linker job the driver would.
|
|
const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false);
|
|
const job = jobs?.link(
|
|
[...plan.inputs, stagedOutput, ...(mapArgs.length === 0 ? [] : [linkMap])],
|
|
stage,
|
|
);
|
|
const cacheable = executableCache?.trace(linkArgs, stage, job) ?? false;
|
|
timing("link-inputs");
|
|
// A successful dependency trace already produced this executable.
|
|
// Keep its verified output instead of asking the linker to repeat it.
|
|
if (!cacheable && (job?.run(linkArgs, true) ?? null) === null) {
|
|
const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker;
|
|
try {
|
|
try {
|
|
runNativeTool(driver, linkArgs);
|
|
} catch (error) {
|
|
// A failed link with lld runs again with the driver's default
|
|
// linker, whose error (if any) is the one reported.
|
|
if (lld.length === 0) throw error;
|
|
runNativeTool(driver, linkArgsWith(false));
|
|
}
|
|
} catch (error) {
|
|
const note = staticLinkFailureNote(target, options.staticLink === true);
|
|
if (note === "") throw error;
|
|
throw new Error(`${error instanceof Error ? error.message : String(error)}${note}`);
|
|
}
|
|
}
|
|
timing("native-link");
|
|
requireNativeArtifact(stagedOutput);
|
|
if (hugeText) rewriteHugeTextExecutable(stagedOutput);
|
|
if (mapArgs.length !== 0) {
|
|
const refusal = staticExecutableRefusal(target, true, readFileSync(linkMap, "utf8"));
|
|
if (refusal !== null) throw new NativeCodegenError("SC3002", refusal);
|
|
}
|
|
if (target.platform === "darwin" && optimization === "dev" && !options.strip) {
|
|
runNativeTool(toolchain.dsymutil, [stagedOutput, "-o", stagedOutput + ".dSYM"]);
|
|
}
|
|
if (cacheable) executableCache?.publish(stagedOutput);
|
|
}
|
|
if (target.platform === "darwin" && optimization === "dev" && !options.strip) {
|
|
rmSync(output + ".dSYM", { recursive: true, force: true });
|
|
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
|
|
}
|
|
mkdirSync(options.outDir, { recursive: true });
|
|
writeTextPartsSync(llvmPath, llvm);
|
|
let irPath: string | undefined;
|
|
if (options.emitIr) {
|
|
irPath = join(options.outDir, `${stem}.ir.json`);
|
|
writeTextPartsSync(irPath, prepared.ir!);
|
|
}
|
|
if (!kept) {
|
|
renameSync(stagedOutput, output);
|
|
executableCache?.recordOutput(output);
|
|
}
|
|
if (target.platform === "darwin" && (optimization !== "dev" || options.strip))
|
|
rmSync(output + ".dSYM", { recursive: true, force: true });
|
|
return {
|
|
ok: true,
|
|
artifact: {
|
|
kind: "exe",
|
|
path: output,
|
|
translationUnitPath: llvmPath,
|
|
backend: "llvm",
|
|
...(profileMatch === undefined ? {} : { profileMatch }),
|
|
},
|
|
binaryPath: output,
|
|
llvmPath,
|
|
backend: "llvm",
|
|
...(irPath === undefined ? {} : { irPath }),
|
|
};
|
|
} catch (error) {
|
|
// Keep subclass reads at the catch boundary, where the thrown object
|
|
// retains its class identity and source location.
|
|
if (error instanceof LlvmUnsupportedError)
|
|
return { ok: false, diagnostics: [llvmRefusalDiag(error, entry)], sourceTexts };
|
|
if (error instanceof NativeCodegenError)
|
|
return {
|
|
ok: false,
|
|
diagnostics: [nativeCodegenDiag(error.diagnosticCode, error.message, entry)],
|
|
sourceTexts,
|
|
};
|
|
if (error instanceof RuntimePackError)
|
|
return {
|
|
ok: false,
|
|
diagnostics: [
|
|
nativeCodegenDiag(
|
|
error.code === "unsupported" ? "SC3002" : "SC3003",
|
|
error.message,
|
|
entry,
|
|
error.hint,
|
|
),
|
|
],
|
|
sourceTexts,
|
|
};
|
|
return failure(error, entry, sourceTexts);
|
|
} finally {
|
|
if (stage !== null) rmSync(stage, { recursive: true, force: true });
|
|
this.cache?.prune();
|
|
timing("complete");
|
|
}
|
|
}
|
|
|
|
compileLibrary(options: CompileLibraryOptions): CompileLibraryResult {
|
|
const timing = compilationTiming();
|
|
clearFenceEvalCaches();
|
|
const toolchain = this.toolchain;
|
|
let sourceTexts = new Map<string, string>();
|
|
let stage: string | null = null;
|
|
try {
|
|
const loaded = loadLibraryProfile(resolve(options.profilePath));
|
|
if (!loaded.ok) return { ok: false, diagnostics: loaded.diagnostics, sourceTexts };
|
|
const profile = loaded.profile;
|
|
const wasm = toolchain.target.platform === "wasi";
|
|
if (wasm) {
|
|
const refusal = libraryWasmRefusal(profile, options.sanitize ?? false);
|
|
if (refusal !== null) return { ok: false, diagnostics: [refusal], sourceTexts };
|
|
}
|
|
if (options.sanitize) sanitizerDriver(toolchain);
|
|
const stem = basename(profile.entry).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
|
const archivePath = resolve(
|
|
options.outPath ?? join(options.outDir, `${stem}${wasm ? ".wasm" : ".lib.a"}`),
|
|
);
|
|
const llvmPath = join(options.outDir, `${stem}.lib.ll`);
|
|
const prepared = prepareNativeLibrary(
|
|
profile,
|
|
options,
|
|
archivePath,
|
|
toolchain,
|
|
this.frontend,
|
|
this.cache,
|
|
timing,
|
|
);
|
|
if (!prepared.ok) return prepared;
|
|
sourceTexts = new Map(prepared.sources);
|
|
if (archivePath === profile.entry)
|
|
throw new Error("output path must differ from the entry source");
|
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
mkdirSync(options.outDir, { recursive: true });
|
|
stage = mkdtempSync(join(dirname(archivePath), ".scriptc-library-"));
|
|
const llvm = prepared.llvm;
|
|
const llvmInput = join(stage, "program.ll");
|
|
// In slices: the module text is never encoded whole.
|
|
writeTextPartsSync(llvmInput, llvm);
|
|
timing("llvm-write");
|
|
const features = prepared.features;
|
|
const pack = selectNativeRuntimePack(
|
|
toolchain.runtimePackRoot,
|
|
toolchain.target,
|
|
toolchain.compilerVersion,
|
|
features,
|
|
profile.optimization,
|
|
profile.instancePerThread ? "library-thread" : "library",
|
|
this.runtimePackStore(),
|
|
);
|
|
const runtime = options.sanitize
|
|
? buildSanitizedRuntime(
|
|
toolchain,
|
|
pack,
|
|
join(stage, "runtime"),
|
|
profile.instancePerThread ? "library-thread" : "library",
|
|
this.cache,
|
|
)
|
|
: stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore());
|
|
timing("runtime-stage");
|
|
const stagedArchive = join(stage, wasm ? "output.wasm" : "output.a");
|
|
const localizeSymbols = libraryLocalizeSymbols(profile);
|
|
const program = join(stage, "program" + toolchain.target.outputSuffixes.obj);
|
|
if (options.sanitize) {
|
|
runNativeTool(sanitizerDriver(toolchain), [
|
|
...sanitizerFlags(toolchain, profile.optimization),
|
|
"-c",
|
|
llvmInput,
|
|
"-o",
|
|
program,
|
|
]);
|
|
requireNativeArtifact(program);
|
|
} else
|
|
this.emitProgramObject(
|
|
llvmInput,
|
|
program,
|
|
profile.entry,
|
|
profile.optimization,
|
|
stage,
|
|
true,
|
|
);
|
|
timing("native-object");
|
|
if (wasm) {
|
|
linkNativeWasmLibrary({
|
|
toolchain,
|
|
programObject: program,
|
|
outputPath: stagedArchive,
|
|
runtime,
|
|
optimization: profile.optimization,
|
|
exports: libraryWasmExports(profile),
|
|
});
|
|
} else {
|
|
archiveNativeLibrary({
|
|
toolchain,
|
|
programObject: program,
|
|
outputPath: stagedArchive,
|
|
stage,
|
|
runtime,
|
|
...(localizeSymbols === undefined ? {} : { localizeSymbols }),
|
|
});
|
|
}
|
|
timing("native-archive");
|
|
writeTextPartsSync(llvmPath, llvm);
|
|
let irPath: string | undefined;
|
|
if (options.emitIr) {
|
|
irPath = join(options.outDir, `${stem}.lib.ir.json`);
|
|
writeTextPartsSync(irPath, prepared.ir!);
|
|
}
|
|
let sidecarPath: string | undefined;
|
|
if (prepared.sidecarJson !== null) {
|
|
sidecarPath =
|
|
profile.sidecar!.path === null
|
|
? `${archivePath}.contract.json`
|
|
: resolve(dirname(archivePath), profile.sidecar!.path);
|
|
writeFileSync(sidecarPath, prepared.sidecarJson);
|
|
}
|
|
renameSync(stagedArchive, archivePath);
|
|
return {
|
|
ok: true,
|
|
archivePath,
|
|
llvmPath,
|
|
backend: "llvm",
|
|
...(irPath === undefined ? {} : { irPath }),
|
|
...(sidecarPath === undefined ? {} : { sidecarPath }),
|
|
};
|
|
} catch (error) {
|
|
// Keep subclass reads at the catch boundary, where the thrown object
|
|
// retains its class identity and source location.
|
|
if (error instanceof LlvmUnsupportedError)
|
|
return {
|
|
ok: false,
|
|
diagnostics: [llvmRefusalDiag(error, options.profilePath)],
|
|
sourceTexts,
|
|
};
|
|
if (error instanceof NativeCodegenError)
|
|
return {
|
|
ok: false,
|
|
diagnostics: [
|
|
nativeCodegenDiag(error.diagnosticCode, error.message, options.profilePath),
|
|
],
|
|
sourceTexts,
|
|
};
|
|
if (error instanceof RuntimePackError)
|
|
return {
|
|
ok: false,
|
|
diagnostics: [
|
|
nativeCodegenDiag(
|
|
error.code === "unsupported" ? "SC3002" : "SC3003",
|
|
error.message,
|
|
options.profilePath,
|
|
error.hint,
|
|
),
|
|
],
|
|
sourceTexts,
|
|
};
|
|
return failure(error, options.profilePath, sourceTexts);
|
|
} finally {
|
|
if (stage !== null) rmSync(stage, { recursive: true, force: true });
|
|
this.cache?.prune();
|
|
timing("complete");
|
|
}
|
|
}
|
|
}
|