Files
scriptc/packages/compiler/src/cli/command.ts
T
Chris Tate f591e8e6d7 Print how a --profile-use build's profile matched
`scriptc build --profile-use` prints one line on stderr with the number of
functions the profile annotated, the number whose control flow changed since
it was collected (mismatched), and the number it has no record of. When more
than 10% of the functions the profile describes are mismatched, it also prints
a warning to collect the profile again. The build still succeeds.

Both the compiler library's build and the native compiler's build report the
match. Cached builds report the match of the build that produced them: the
native codegen and object caches store it beside the cached objects, and the
executable cache stores it in its entry.
2026-10-25 22:14:25 -05:00

773 lines
31 KiB
TypeScript

import type { TypePatchCommandRequest } from "../frontend/type-patches/command.js";
import { rmSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { parseArgs } from "node:util";
import { isExactExternalTypeSpecifier } from "../frontend/program.js";
import { renderDiagnostics } from "../diagnostics/render.js";
import {
COVERAGE_FAIL_ON,
coverageEnvelope,
coveragePasses,
renderCoverage,
renderExplainDynamic,
renderExplainIsland,
type CoverageFailOn,
} from "../coverage/report.js";
import {
NODE_ISLANDS_MODES,
type NodeIslandOptions,
type NodeIslandsMode,
} from "../node-islands/policy.js";
import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
import { buildEnvelope } from "../diagnostics/envelope.js";
import {
renderConfigWarnings,
renderTypePatchWarnings,
renderWarnings,
} from "../diagnostics/render.js";
import { isTypePatchCode } from "../diagnostics/diagnostic.js";
import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js";
import { isNativeOptimization } from "../backend/optimization.js";
import { renderNativeProfileMatch } from "../backend/native-profile.js";
import {
hostTargetCpu,
isTargetCpuOption,
TARGET_CPU_OPTIONS,
targetCpuField,
type TargetCpu,
} from "../backend/target-cpu.js";
import { setProvenanceSources } from "../frontend/provenance-registry.js";
import type { CliHost, NativeCacheWarmProfile } from "./host.js";
import { resolveOutputOptions } from "./output-options.js";
import { selectOutputPaths } from "./paths.js";
import { CLI_OPTIONS, USAGE } from "./usage.js";
/* The exit discipline: NEVER process.exit() after writing output. stdout/
* stderr to a PIPE are async streams — process.exit() drops whatever libuv
* hasn't flushed yet, which truncates large diagnostic renders at the pipe
* buffer (observed: 64KB cut mid-code-frame). Every path sets
* process.exitCode and returns instead; Node exits naturally once the
* streams drain. */
class CliExit extends Error {
constructor(readonly code: number) {
super(`exit ${code}`);
this.name = "CliExit";
}
}
function fail(msg: string): never {
process.stderr.write(msg + "\n");
throw new CliExit(1);
}
/** parseArgs, with its throw turned into the CLI's own one-line error.
* Unparseable arguments are a USER error — an unknown flag or a missing
* value used to reach the top level as an uncaught ERR_PARSE_ARGS_* and
* print a Node stack trace over the user's terminal. */
function parseCli(
args: string[],
): ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
> {
// `--explain-dynamic` takes an optional value: alone it lists every unit.
const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg));
try {
return parseArgs({
args: normalized,
options: CLI_OPTIONS,
allowPositionals: true,
allowNegative: true,
});
} catch (err) {
// parseArgs appends a paragraph about `--` and positionals to the
// unknown-option message; the first sentence is the part that names
// what was wrong, and USAGE below already covers what was meant.
const raw = err instanceof Error ? err.message : String(err);
const msg = raw.split("\n")[0]!.split(". ")[0]!;
fail(`scriptc: ${msg}\n\n${USAGE}`);
}
}
async function main(args: string[], host: CliHost): Promise<number> {
const { values, positionals } = parseCli(args);
const externalTypeArgs = values["external-types"] ?? [];
if (values.version) {
process.stdout.write(`${host.version()}\n`);
return 0;
}
if (values.help || positionals.length === 0) {
process.stdout.write(USAGE);
return values.help ? 0 : 1;
}
const [command, inputArg] = positionals;
if (command === "cache") {
if (inputArg !== "warm")
fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`);
if (
values.lib ||
values.backend !== undefined ||
values.emit !== undefined ||
values.print !== undefined ||
values["fail-on"] !== undefined ||
values.ffi !== undefined ||
values.profile !== undefined ||
values.strip ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
(values["npm-static"] ?? []).length > 0 ||
values["provenance-sources"] ||
externalTypeArgs.length > 0 ||
values.out !== undefined ||
values["emit-ir"] ||
values.tier !== undefined ||
values["explain-dynamic"] !== undefined ||
hasNodeIslandFlags(values) ||
!values["keep-llvm"]
) {
fail(
`scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`,
);
}
const optimization = values.optimization;
if (optimization !== undefined && !isNativeOptimization(optimization)) {
fail(`unknown optimization "${optimization}" (supported: release, dev, speed)\n\n${USAGE}`);
}
const profileArgs = positionals.slice(2);
const knownProfiles = new Set<NativeCacheWarmProfile>(["runtime", "tls"]);
for (const profile of profileArgs) {
if (!knownProfiles.has(profile as NativeCacheWarmProfile)) {
fail(`unknown cache warm profile "${profile}" (supported: runtime, tls)`);
}
}
let result;
try {
result = await host.warmNativeCaches({
...(optimization === undefined ? {} : { optimization }),
sanitize: values.sanitize,
...(profileArgs.length === 0 ? {} : { profiles: profileArgs as NativeCacheWarmProfile[] }),
});
} catch (error) {
fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`);
}
process.stdout.write(`${result.cacheRoot}\n`);
for (const profile of result.profiles) {
process.stdout.write(`${profile.profile}\t${Math.round(profile.elapsedMs)}ms\n`);
}
return 0;
}
if (command === "merge-profiles") {
const inputs = positionals.slice(1);
const others =
values.lib ||
values.backend !== undefined ||
values.emit !== undefined ||
values.print !== undefined ||
values["fail-on"] !== undefined ||
values.optimization !== undefined ||
values.ffi !== undefined ||
values.profile !== undefined ||
values.strip ||
values.sanitize ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
values["provenance-sources"] ||
externalTypeArgs.length > 0 ||
(values.wasm ?? []).length > 0 ||
values["emit-ir"] ||
values.tier !== undefined ||
values["explain-dynamic"] !== undefined ||
hasNodeIslandFlags(values) ||
!values["keep-llvm"];
if (values.out === undefined || inputs.length === 0 || others) {
fail(
`scriptc merge-profiles takes -o <out.profdata>, optionally --sparse, and one or more profiles\n\n${USAGE}`,
);
}
const outputPath = resolve(values.out);
try {
await host.mergeProfiles(
outputPath,
inputs.map((input) => resolve(input)),
values.sparse,
);
} catch (error) {
fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`);
}
process.stdout.write(`${outputPath}\n`);
return 0;
}
if (command === "types") {
const sub = positionals[1];
const rest = positionals.slice(2);
let request: TypePatchCommandRequest;
if (sub === "patch" && rest.length === 3) {
request = {
action: "patch",
cwd: process.cwd(),
pkg: rest[0]!,
selector: rest[1]!,
type: rest[2]!,
range: values.range ?? null,
dryRun: values["dry-run"],
};
} else if (
sub === "list" &&
rest.length === 0 &&
values.range === undefined &&
!values["dry-run"]
) {
request = { action: "list", cwd: process.cwd() };
} else {
fail(
`scriptc types takes "patch <package> <selector> <type>" (with --range or --dry-run) or "list"\n\n${USAGE}`,
);
}
const result = await host.typePatches(request);
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
return result.exitCode;
}
if (values.range !== undefined || values["dry-run"])
fail(`--range and --dry-run are scriptc types patch options\n\n${USAGE}`);
if (values.sparse) fail(`--sparse is a merge-profiles option\n\n${USAGE}`);
if (command !== "build" && command !== "run" && command !== "coverage") {
fail(`unknown command "${command}"\n\n${USAGE}`);
}
if (values.lib) {
// LIBRARY mode: the profile names the entry module and pins the
// emission; the executable lane's mode flags have no meaning here
// (library artifacts are static-tier only, and there is no fallback
// concept — bare npm specifiers are static-or-refuse: the npm-static
// eligibility bar runs automatically, eligible packages compile into
// the graph, ineligible ones refuse with SC4013).
if (command !== "build")
fail(`--lib is a build mode (scriptc build --lib --profile <p.json>)\n\n${USAGE}`);
const profileArg = values.profile;
if (!profileArg) fail(`scriptc build --lib needs --profile <profile.json>\n\n${USAGE}`);
if (inputArg) {
fail("scriptc build --lib takes no input positional: the profile names the entry module");
}
if (
values.backend !== undefined ||
values.emit !== undefined ||
values.print !== undefined ||
values["fail-on"] !== undefined ||
values.optimization !== undefined ||
values.strip ||
values.ffi !== undefined ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values["cpu-dispatch"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
externalTypeArgs.length > 0 ||
values.tier !== undefined ||
values["explain-dynamic"] !== undefined ||
hasNodeIslandFlags(values)
) {
fail(
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--cpu-dispatch/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
);
}
const profilePath = resolve(profileArg);
const libOutDir = values.out
? dirname(resolve(values.out))
: join(dirname(profilePath), ".scriptc");
const result = await host.compileLibrary({
profilePath,
outDir: libOutDir,
...(values.out ? { outPath: resolve(values.out) } : {}),
emitIr: values["emit-ir"],
sanitize: values.sanitize,
});
if (!result.ok) {
const color = process.stderr.isTTY ?? false;
process.stderr.write(
renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n",
);
const n = result.diagnostics.length;
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
return 1;
}
if (!values["keep-llvm"]) rmSync(result.llvmPath, { force: true });
process.stdout.write(`${result.archivePath}\n`);
// The contract sidecar rides the same invocation when the profile
// declares one — name it so the embedder's tooling knows where to look.
if (result.sidecarPath !== undefined) process.stdout.write(`${result.sidecarPath}\n`);
return 0;
}
if (values["emit-ir"] && (command === "build" || command === "run")) {
process.stderr.write(
"scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n",
);
}
if (!inputArg) fail(`missing input file\n\n${USAGE}`);
const input = resolve(inputArg);
if (command === "coverage" && values.emit !== undefined) {
fail(`--emit is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values.strip) {
fail(`--strip is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["target-cpu"] !== undefined) {
fail(`--target-cpu is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["cpu-dispatch"] !== undefined) {
fail(`--cpu-dispatch is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["detached-exit"]) {
fail(`--detached-exit is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["profile-generate"] !== undefined) {
fail(`--profile-generate is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["profile-use"] !== undefined) {
fail(`--profile-use is a build/run option\n\n${USAGE}`);
}
if (
values.print !== undefined &&
values.print !== "native-link-info" &&
values.print !== "diagnostics"
) {
fail(
`unknown print kind "${values.print}" (supported: native-link-info, diagnostics)\n\n${USAGE}`,
);
}
const printNativeLinkInfo = values.print === "native-link-info";
const printDiagnostics = values.print === "diagnostics";
if (printDiagnostics && command === "run") {
fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`);
}
const failOnRaw = values["fail-on"];
if (failOnRaw !== undefined && !(COVERAGE_FAIL_ON as readonly string[]).includes(failOnRaw)) {
fail(
`unknown --fail-on value "${failOnRaw}" (supported: ${COVERAGE_FAIL_ON.join(", ")})\n\n${USAGE}`,
);
}
// --no-dynamic-tier-warning leaves out SC6101 (diagnostics/dynamic-tier.ts).
const dynamicTierWarning = values["dynamic-tier-warning"] !== false;
if (failOnRaw === "dynamic" && !dynamicTierWarning) {
fail(`--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out\n\n${USAGE}`);
}
if (failOnRaw !== undefined && command !== "coverage") {
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
}
const failOn = failOnRaw as CoverageFailOn | undefined;
const tierRaw = values.tier;
if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) {
fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`);
}
const tier = tierRaw as TierMode | undefined;
const explainDynamic = values["explain-dynamic"];
if (explainDynamic !== undefined && command !== "coverage") {
fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
}
const explainIsland = values["explain-island"];
if (explainIsland !== undefined && command !== "coverage") {
fail(`--explain-island is a coverage option\n\n${USAGE}`);
}
if (explainIsland === "") fail(`--explain-island needs a package name\n\n${USAGE}`);
if (values["strict-user-js"] && command !== "coverage") {
fail(`--strict-user-js is a coverage option\n\n${USAGE}`);
}
// The Node island policy's command-line half. Report-only: build and run
// validate it, coverage reports the islands it would choose.
const islandMode = values["node-islands"];
if (islandMode !== undefined && !(NODE_ISLANDS_MODES as readonly string[]).includes(islandMode)) {
fail(
`unknown --node-islands value "${islandMode}" (supported: ${NODE_ISLANDS_MODES.join(", ")})\n\n${USAGE}`,
);
}
const listValues = (raw: string[] | undefined): string[] =>
(raw ?? [])
.flatMap((v) => v.split(","))
.map((v) => v.trim())
.filter((v) => v !== "");
const islandArgs = listValues(values["node-island"]);
const nativeArgs = listValues(values.native);
for (const name of nativeArgs) {
if (islandArgs.includes(name))
fail(`--native=${name} and --node-island=${name} contradict each other\n\n${USAGE}`);
}
if (values.node === "") fail(`--node needs the path of a node executable\n\n${USAGE}`);
const nodeIslands: NodeIslandOptions | undefined =
islandMode === undefined &&
islandArgs.length === 0 &&
nativeArgs.length === 0 &&
values.node === undefined &&
!values["strict-user-js"]
? undefined
: {
...(islandMode !== undefined ? { mode: islandMode as NodeIslandsMode } : {}),
...(islandArgs.length > 0 ? { islands: islandArgs } : {}),
...(nativeArgs.length > 0 ? { native: nativeArgs } : {}),
...(values.node !== undefined ? { node: resolve(values.node) } : {}),
...(values["strict-user-js"] ? { strictUserJs: true } : {}),
};
if (printNativeLinkInfo && command !== "build") {
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
}
if (printNativeLinkInfo && values.emit !== undefined && values.emit !== "obj") {
fail(`--print=native-link-info requires --emit=obj\n\n${USAGE}`);
}
if (externalTypeArgs.length > 0 && command !== "coverage") {
fail(`--external-types is a coverage-only option\n\n${USAGE}`);
}
const externalTypes: Record<string, string> = Object.create(null) as Record<string, string>;
for (const mapping of externalTypeArgs) {
const equals = mapping.indexOf("=");
if (equals <= 0 || equals === mapping.length - 1) {
fail(
`invalid --external-types mapping ${JSON.stringify(mapping)} (expected <specifier=file.d.ts>)`,
);
}
const specifier = mapping.slice(0, equals).trim();
const declarationArg = mapping.slice(equals + 1).trim();
if (!isExactExternalTypeSpecifier(specifier)) {
fail(
`invalid --external-types specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`,
);
}
if (!/\.d\.(?:ts|mts|cts)$/.test(declarationArg)) {
fail(
`invalid --external-types declaration ${JSON.stringify(declarationArg)} (expected a .d.ts, .d.mts, or .d.cts file)`,
);
}
if (externalTypes[specifier] !== undefined) {
fail(`duplicate --external-types mapping for ${JSON.stringify(specifier)}`);
}
const declarationPath = resolve(declarationArg);
try {
if (!statSync(declarationPath).isFile()) throw new Error("not a file");
} catch {
fail(`--external-types declaration does not name a readable file: ${declarationPath}`);
}
externalTypes[specifier] = declarationPath;
}
const wasmArgs = values.wasm ?? [];
if (wasmArgs.length > 0 && command !== "coverage") {
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
}
const wasmFiles = wasmArgs.map((file) => resolve(file));
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
if (values.backend !== undefined && values.backend !== "llvm") {
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
}
const optimization = values.optimization;
if (optimization !== undefined && !isNativeOptimization(optimization)) {
fail(`unknown optimization "${optimization}" (supported: release, dev, speed)\n\n${USAGE}`);
}
const windowsSubsystem = values["windows-subsystem"];
if (
windowsSubsystem !== undefined &&
windowsSubsystem !== "console" &&
windowsSubsystem !== "gui"
) {
fail(`unknown Windows subsystem "${windowsSubsystem}" (supported: console, gui)\n\n${USAGE}`);
}
if (windowsSubsystem !== undefined && command === "coverage") {
fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`);
}
const targetCpuOption = values["target-cpu"];
if (targetCpuOption !== undefined && !isTargetCpuOption(targetCpuOption)) {
fail(
`unknown target CPU "${targetCpuOption}" (supported: ${TARGET_CPU_OPTIONS.join(", ")})\n\n${USAGE}`,
);
}
let targetCpu: TargetCpu | undefined = targetCpuOption === "native" ? undefined : targetCpuOption;
if (targetCpuOption === "native") {
const level = hostTargetCpu();
if (level === null)
fail(`--target-cpu=native needs an x86-64 Linux or macOS build host; name a level instead`);
targetCpu = level;
}
const output =
command === "coverage"
? null
: resolveOutputOptions(command, {
...(values.emit === undefined && !printNativeLinkInfo
? {}
: { emit: values.emit ?? "obj" }),
emitIr: values["emit-ir"],
...(values.backend === undefined ? {} : { backend: values.backend }),
keepLlvm: values["keep-llvm"],
sanitize: values.sanitize,
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
strip: values.strip,
detachedExit: values["detached-exit"],
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
...(values["profile-generate"] === undefined
? {}
: { profileGenerate: values["profile-generate"] }),
...(values["profile-use"] === undefined ? {} : { profileUse: values["profile-use"] }),
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
});
if (output !== null && !output.ok) fail(`${output.message}\n\n${USAGE}`);
const backend = output === null ? undefined : output.backend;
// --npm-static: repeatable and comma-splittable; the literal "auto"
// switches to eligibility-based detection (mixing "auto" with names
// is rejected — the shapes answer different questions).
const npmStaticRaw = (values["npm-static"] ?? [])
.flatMap((v) => v.split(","))
.map((v) => v.trim())
.filter((v) => v !== "");
let npmStatic: string[] | "auto" | undefined;
if (npmStaticRaw.includes("auto")) {
if (npmStaticRaw.length > 1)
fail(`--npm-static auto cannot be combined with package names\n\n${USAGE}`);
npmStatic = "auto";
} else if (npmStaticRaw.length > 0) {
npmStatic = npmStaticRaw;
}
// --provenance-sources resolves BEFORE the program loads (tsgo needs the
// source "paths" at creation): attestations and source trees fetch (or
// ride the content-addressed cache / the offline manifest), the registry
// installs, and every fallback prints as a note — never a failure.
const provenance = values["provenance-sources"]
? await host.resolveProvenanceSources(input)
: null;
if (provenance !== null) {
setProvenanceSources(provenance);
for (const pkg of provenance.packages) {
process.stderr.write(
`provenance: ${pkg.name}@${pkg.version} ← ${pkg.repo.replace(/^git\+/, "")} @ ${pkg.commit.slice(0, 12)} (source compiles statically)\n`,
);
}
for (const note of provenance.notes) process.stderr.write(`provenance: ${note}\n`);
}
if (command === "coverage") {
const { coverage, sourceTexts } = await host.analyze(input, {
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
...(tier !== undefined ? { tier } : {}),
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
...(nodeIslands !== undefined ? { nodeIslands } : {}),
});
if (!dynamicTierWarning) delete coverage.performance;
if (printDiagnostics) {
// Keep stdout pure JSON for tooling.
const envelope = coverageEnvelope(coverage, {
compilerVersion: host.version(),
sourceTexts,
...(failOn === undefined ? {} : { failOn }),
});
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
} else {
const color = process.stdout.isTTY ?? false;
process.stdout.write(
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
);
if (explainIsland !== undefined) {
process.stdout.write(
"\n" +
renderExplainIsland(coverage.nodeIslands, explainIsland, {
color,
sourceTexts,
root: process.cwd(),
}) +
"\n",
);
}
if (explainDynamic !== undefined && coverage.tiers !== undefined) {
process.stdout.write(
"\n" +
renderExplainDynamic(coverage.tiers, {
color,
sourceTexts,
root: process.cwd(),
query: explainDynamic,
}) +
"\n",
);
}
}
return coveragePasses(coverage, failOn) ? 0 : 1;
}
if (output === null || !output.ok) throw new Error("internal output-option state");
if (windowsSubsystem !== undefined && host.sourceTargetPlatform() !== "win32") {
fail(`--windows-subsystem requires a Windows executable target\n\n${USAGE}`);
}
const { outDir, outPath } = selectOutputPaths(
input,
output.cliOutputKind,
values.out,
output.cliOutputKind === "exe" ? host.sourceTargetPlatform() : undefined,
);
let nativeLinkInfo: object | undefined;
let diagnosticsEnvelope: object | undefined;
const build = async (): Promise<string> => {
const result = await host.compile(input, {
outPath,
outDir,
outputKind: output.outputKind,
emitIr: output.emitIr,
sanitize: values.sanitize,
...(backend !== undefined ? { backend } : {}),
...(optimization !== undefined ? { optimization } : {}),
...(values.strip ? { strip: true } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(values["detached-exit"] ? { detachedExit: true } : {}),
...targetCpuField(targetCpu),
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
// The instrumented program resolves a relative pattern against its own
// working directory; resolve it here, where the user wrote it.
...(values["profile-generate"] === undefined
? {}
: { profileGenerate: resolve(values["profile-generate"]) }),
...(values["profile-use"] === undefined
? {}
: { profileUse: resolve(values["profile-use"]) }),
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
...(tier !== undefined ? { tier } : {}),
});
if (!result.ok) {
if (printDiagnostics) {
const envelope = buildEnvelope({
compilerVersion: host.version(),
entry: input,
ok: false,
diagnostics: result.diagnostics,
sourceTexts: result.sourceTexts,
});
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
throw new CliExit(1);
}
const color = process.stderr.isTTY ?? false;
process.stderr.write(
renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n",
);
const n = result.diagnostics.length;
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
throw new CliExit(1);
}
const warnings = (result.warnings ?? []).filter(
(d) => dynamicTierWarning || !isPerformanceCode(d.code),
);
if (printDiagnostics) {
diagnosticsEnvelope = buildEnvelope({
compilerVersion: host.version(),
entry: input,
ok: true,
artifact: result.artifact.path,
diagnostics: [],
warnings,
...(result.sourceTexts === undefined ? {} : { sourceTexts: result.sourceTexts }),
});
} else if (warnings.length > 0 && command === "build") {
// Divergence warnings never fail a build; they explain where the
// native program can behave differently from Node. The performance
// warning names likely-hot functions that use untyped values. `run`
// keeps stderr for the program's own output.
const color = process.stderr.isTTY ?? false;
const config = warnings.filter((d) => d.code === "SC8004");
const patches = warnings.filter((d) => isTypePatchCode(d.code));
const divergences = warnings.filter(
(d) => !isPerformanceCode(d.code) && d.code !== "SC8004" && !isTypePatchCode(d.code),
);
const performance = warnings.filter((d) => isPerformanceCode(d.code));
if (config.length > 0)
process.stderr.write(
renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n",
);
if (patches.length > 0) {
if (config.length > 0) process.stderr.write("\n");
process.stderr.write(
renderTypePatchWarnings(patches, result.sourceTexts ?? new Map(), { color }) + "\n",
);
}
if (divergences.length > 0) {
if (config.length > 0 || patches.length > 0) process.stderr.write("\n");
process.stderr.write(
renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n",
);
}
if (performance.length > 0) {
if (divergences.length > 0 || config.length > 0 || patches.length > 0)
process.stderr.write("\n");
process.stderr.write(renderPerformanceWarnings(performance) + "\n");
}
}
if (result.artifact.kind === "exe") {
// How the profile matched the program's functions; `run` keeps stderr
// for the program's own output.
const profileUse = values["profile-use"];
if (
command === "build" &&
profileUse !== undefined &&
result.artifact.profileMatch !== undefined
)
process.stderr.write(renderNativeProfileMatch(profileUse, result.artifact.profileMatch));
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
} else if (result.artifact.kind === "obj") {
nativeLinkInfo = result.artifact.nativeLinkInfo;
}
return result.artifact.path;
};
const binary = await build();
if (command === "run") {
return host.run(binary);
}
if (printDiagnostics) {
process.stdout.write(`${JSON.stringify(diagnosticsEnvelope, null, 2)}\n`);
} else if (printNativeLinkInfo) {
if (nativeLinkInfo === undefined) throw new Error("internal native-link-info state");
// Keep stdout pure JSON for tooling; the ordinary artifact path is in
// program.object inside the document.
process.stdout.write(`${JSON.stringify(nativeLinkInfo, null, 2)}\n`);
} else {
process.stdout.write(`${binary}\n`);
}
return 0;
}
/** Whether any Node island flag is present (they belong to build, run and
* coverage). */
function hasNodeIslandFlags(values: ReturnType<typeof parseCli>["values"]): boolean {
return (
values["node-islands"] !== undefined ||
(values["node-island"] ?? []).length > 0 ||
(values.native ?? []).length > 0 ||
values.node !== undefined ||
values["explain-island"] !== undefined ||
values["strict-user-js"]
);
}
/** Both installed and seed commands use this argument and diagnostic contract. */
export async function runCli(args: string[], host: CliHost): Promise<number> {
try {
return await main(args, host);
} catch (err) {
if (err instanceof Error && err.name === "CliExit") return Number(err.message.slice(5));
throw err;
}
}