diff --git a/README.md b/README.md index 35c002669..57ebcb350 100644 --- a/README.md +++ b/README.md @@ -54,17 +54,29 @@ hello.ir.json $ scriptc build hello.ts --emit=c >/dev/null $ ls .scriptc/ hello.c +hello.ir.json $ scriptc build hello.ts --emit=llvm >/dev/null $ ls .scriptc/ +hello.c +hello.ir.json hello.ll $ scriptc build hello.ts --emit=asm >/dev/null $ ls .scriptc/ +hello.c +hello.ir.json +hello.ll hello.s $ scriptc build hello.ts --emit=obj >/dev/null $ ls .scriptc/ +hello.c +hello.ir.json +hello.ll hello.o +hello.s ``` +Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. + `--emit=obj` writes a relocatable program object, not a standalone library. It has undefined `scr_*` runtime references and a required `scr_runtime_abi_v1` marker; `scriptc build --lib --profile ...` remains the diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index 4050260ce..ca383b76c 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -36,17 +36,29 @@ fib.ir.json $ scriptc build fib.ts --emit=c >/dev/null $ ls .scriptc/ fib.c +fib.ir.json $ scriptc build fib.ts --emit=llvm >/dev/null $ ls .scriptc/ +fib.c +fib.ir.json fib.ll $ scriptc build fib.ts --emit=asm >/dev/null $ ls .scriptc/ +fib.c +fib.ir.json +fib.ll fib.s $ scriptc build fib.ts --emit=obj >/dev/null $ ls .scriptc/ +fib.c +fib.ir.json +fib.ll fib.o +fib.s ``` +Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. + The three source outputs require only Node. On macOS 15+ arm64, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a diff --git a/docs/src/app/how-it-works/page.mdx b/docs/src/app/how-it-works/page.mdx index 1ebb3f055..407dd6bd3 100644 --- a/docs/src/app/how-it-works/page.mdx +++ b/docs/src/app/how-it-works/page.mdx @@ -30,15 +30,25 @@ fib.ir.json $ scriptc build fib.ts --emit=c $ ls .scriptc/ fib.c +fib.ir.json $ scriptc build fib.ts --emit=llvm $ ls .scriptc/ +fib.c +fib.ir.json fib.ll $ scriptc build fib.ts --emit=asm $ ls .scriptc/ +fib.c +fib.ir.json +fib.ll fib.s $ scriptc build fib.ts --emit=obj $ ls .scriptc/ +fib.c +fib.ir.json +fib.ll fib.o +fib.s ``` ## The runtime diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index c67c2c8cd..fa490bd95 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -151,10 +151,6 @@ async function tryFastPath(): Promise { if (hit.native.llvmRefusal !== undefined) { process.stderr.write(`scriptc: backend c (llvm refused: ${hit.native.llvmRefusal})\n`); } - // Source-primary invocations can replace a previously cached executable. - // A routed hit restores that executable without loading the full compiler, - // so mirror its output-kind cleanup before returning from the fast path. - await rm(join(outDir, `${stem}.ir.json`), { force: true }); if (!values["keep-c"]) await rm(hit.cPath, { force: true }); if (command === "build") { process.stdout.write(`${outPath}\n`); diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index e965faadb..86beaf7c5 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -269,7 +269,7 @@ async function main(): Promise { if (windowsSubsystem !== undefined && sourceTargetPlatform() !== "win32") { fail(`--windows-subsystem requires a Windows executable target\n\n${USAGE}`); } - const { outDir, outPath, defaultOutputPath } = selectOutputPaths(input, output.cliOutputKind, values.out); + const { outDir, outPath } = selectOutputPaths(input, output.cliOutputKind, values.out); // SCRIPTC_CC remains a migration escape hatch for explicit C, sanitizer, // and comparison builds. The normal LLVM executable route is controlled by @@ -303,7 +303,6 @@ async function main(): Promise { outPath, outDir, outputKind: output.outputKind, - defaultOutputPath, emitIr: output.emitIr, sanitize: values.sanitize, dynamic: values.dynamic, diff --git a/packages/cli/src/paths.ts b/packages/cli/src/paths.ts index 4107e8f52..08e139cbd 100644 --- a/packages/cli/src/paths.ts +++ b/packages/cli/src/paths.ts @@ -33,7 +33,6 @@ export function defaultOutputName( export interface OutputPaths { outDir: string; outPath: string; - defaultOutputPath: boolean; } /** One authority for explicit and default primary artifact paths. */ @@ -53,7 +52,6 @@ export function selectOutputPaths( outPath: explicitOut === undefined ? join(outDir, defaultOutputName(stem, kind, platform)) : resolve(explicitOut), - defaultOutputPath: explicitOut === undefined, }; } diff --git a/packages/cli/test/bootstrap.test.ts b/packages/cli/test/bootstrap.test.ts index e30c02b64..8b6d31e73 100644 --- a/packages/cli/test/bootstrap.test.ts +++ b/packages/cli/test/bootstrap.test.ts @@ -71,13 +71,14 @@ test("bootstrap exact builds use the routed cache and source edits fall through" expect((await build()).stderr).not.toContain("scriptc lowering"); expect((await execFileAsync(outPath)).stdout).toBe("one\n"); - // A source-primary build may replace the cached executable between exact - // invocations. The shipped bootstrap must not leave its stale IR sibling - // behind, whether cache validation returns directly or falls through. - const staleIr = join(outDir, "main.ir.json"); - await writeFile(staleIr, "stale source-primary IR\n"); + // A routed cache hit preserves outputs from earlier invocations. + const savedIr = join(outDir, "main.ir.json"); + const savedC = join(outDir, "main.c"); + await writeFile(savedIr, "saved IR\n"); + await writeFile(savedC, "saved C\n"); await expect(build()).resolves.toBeDefined(); - await expect(readFile(staleIr)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(savedIr, "utf8")).toBe("saved IR\n"); + expect(await readFile(savedC, "utf8")).toBe("saved C\n"); // Route metadata can be evicted independently of the executable payload. // One full-compiler fallback must repair it so the following invocation is diff --git a/packages/cli/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index 0dd75f3fb..6127d0370 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -62,13 +62,11 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta expect(await run()).toBe("one\n"); expect((await stat(tuPath)).mtimeMs).toBe(originalTuTime); - // Output-kind cleanup is honest even on an exact early hit: an IR file - // left by a prior additive --emit-ir invocation is not part of a later - // executable-only result. + // An exact early hit leaves earlier source outputs in place. const irPath = join(dir, "main.ir.json"); - await writeFile(irPath, "stale IR\n"); + await writeFile(irPath, "saved IR\n"); expect((await build()).stderr).not.toContain("scriptc lowering"); - await expect(stat(irPath)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(irPath, "utf8")).toBe("saved IR\n"); // Caller damage does not poison the cache: the verified cached binary is // atomically restored without rebuilding the frontend. @@ -90,6 +88,7 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta SCRIPTC_LINKER: join(dir, "first-unused-linker"), })).stderr).toContain("scriptc lowering"); expect(await readFile(join(dir, "main.c"), "utf8")).toContain("Generated by scriptc"); + expect(await readFile(tuPath, "utf8")).toContain("two"); if (runtimePackHost) { // C builds are keyed by their compiler environment. Changing the // object-only linker must not turn an exact C repeat into a miss. @@ -103,6 +102,7 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta expect((await build(["--optimization", "dev"])).stderr).toContain("scriptc lowering"); expect(await run()).toBe("two\n"); expect((await build(["--optimization", "dev"])).stderr).not.toContain("scriptc lowering"); + expect(await readFile(join(dir, "main.c"), "utf8")).toContain("Generated by scriptc"); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/packages/cli/test/paths.test.ts b/packages/cli/test/paths.test.ts index f14464678..0e89a1137 100644 --- a/packages/cli/test/paths.test.ts +++ b/packages/cli/test/paths.test.ts @@ -24,13 +24,11 @@ test("primary output selection keeps explicit paths exact", () => { expect(selectOutputPaths(input, "llvm", undefined, "linux")).toEqual({ outDir: join(input, "../.scriptc"), outPath: join(input, "../.scriptc/main.ll"), - defaultOutputPath: true, }); const explicit = resolve("artifacts/custom.anything"); expect(selectOutputPaths(input, "ir", explicit, "win32")).toEqual({ outDir: join(explicit, ".."), outPath: explicit, - defaultOutputPath: false, }); }); diff --git a/packages/cli/test/source-output.test.ts b/packages/cli/test/source-output.test.ts index 9179a9ae1..2a8cfaf39 100644 --- a/packages/cli/test/source-output.test.ts +++ b/packages/cli/test/source-output.test.ts @@ -36,11 +36,13 @@ function cli(args: string[], env: NodeJS.ProcessEnv = process.env) { test("default and explicit paths identify each source primary artifact", async () => { const { dir, entry } = await fixture(); const expected = { ir: "hello.ir.json", c: "hello.c", llvm: "hello.ll" } as const; + const artifacts: string[] = []; for (const kind of ["ir", "c", "llvm"] as const) { const result = await cli(["build", entry, `--emit=${kind}`]); const path = join(dir, ".scriptc", expected[kind]); + artifacts.push(expected[kind]); expect(result.stdout).toBe(`${path}\n`); - expect(await readdir(join(dir, ".scriptc"))).toEqual([expected[kind]]); + expect((await readdir(join(dir, ".scriptc"))).sort()).toEqual([...artifacts].sort()); } const exact = join(dir, "artifact.with-custom-suffix"); const result = await cli(["build", entry, "--emit=llvm", "-o", exact]); @@ -48,6 +50,18 @@ test("default and explicit paths identify each source primary artifact", async ( expect(await readFile(exact, "utf8")).toContain("define i32 @main"); }); +test("a default source output preserves previously generated sibling artifacts", async () => { + const { dir, entry } = await fixture(); + const outDir = join(dir, ".scriptc"); + await mkdir(outDir); + const siblings = ["hello", "hello.exe", "hello.wasm", "hello.c", "hello.ll", "hello.s", "hello.o"]; + await Promise.all(siblings.map((name) => writeFile(join(outDir, name), `saved ${name}\n`))); + await cli(["build", entry, "--emit=ir"]); + for (const name of siblings) { + await expect(readFile(join(outDir, name), "utf8")).resolves.toBe(`saved ${name}\n`); + } +}); + test("IR CLI output round-trips through the public parser and validator", async () => { const { dir, entry } = await fixture(); const path = join(dir, "hello.json"); diff --git a/packages/cli/test/wasi-runtime-pack.test.ts b/packages/cli/test/wasi-runtime-pack.test.ts index 8b3a454a6..653ce57eb 100644 --- a/packages/cli/test/wasi-runtime-pack.test.ts +++ b/packages/cli/test/wasi-runtime-pack.test.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; @@ -42,3 +42,28 @@ test.runIf(supported)("WASI helper object plus runtime pack builds and runs with encoding: "utf8", })).resolves.toMatchObject({ stdout: "hello world\n" }); }); + +test.runIf(supported)("switching WASI LLVM and native C builds preserves both translation units", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-wasi-native-output-")); + dirs.push(dir); + const entry = join(dir, "hello.ts"); + const outDir = join(dir, ".scriptc"); + const nativeEnv = { ...process.env }; + delete nativeEnv.SCRIPTC_TARGET; + const wasiEnv = { ...nativeEnv, SCRIPTC_TARGET: "wasm32-wasi" }; + const build = (args: string[], env: NodeJS.ProcessEnv) => execFileAsync(process.execPath, [ + "--import", tsxLoader, cliEntry, "build", entry, ...args, + ], { env, maxBuffer: 4 * 1024 * 1024 }); + await writeFile(entry, 'console.log("hello");\n'); + + await build([], wasiEnv); + const llvm = await readFile(join(outDir, "hello.ll")); + const wasm = await readFile(join(outDir, "hello.wasm")); + await build(["--backend=c", "--keep-c"], nativeEnv); + const c = await readFile(join(outDir, "hello.c")); + expect(await readFile(join(outDir, "hello.ll"))).toEqual(llvm); + expect(await readFile(join(outDir, "hello.wasm"))).toEqual(wasm); + + await build([], wasiEnv); + expect(await readFile(join(outDir, "hello.c"))).toEqual(c); +}); diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 447f8ec5e..559305db3 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -111,15 +111,15 @@ test("early executable cache restores the emitted TU, IR, and native feature gat frontend: tracker.snapshot(), }); await Promise.all([rm(f.cPath), rm(f.irPath)]); - const staleC = join(f.options.outDir, "entry.c"); - await writeFile(staleC, "/* stale C backend */\n"); + const alternateC = join(f.options.outDir, "entry.c"); + await writeFile(alternateC, "/* saved C backend */\n"); const hit = await readEarlyExecutableCache(f.root, f.options); expect(hit?.native).toEqual(native); expect(hit?.executableRestored).toBe(false); expect(await readFile(f.cPath, "utf8")).toBe("; generated llvm\n"); expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); - await expect(readFile(staleC, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(alternateC, "utf8")).toBe("/* saved C backend */\n"); }); test("early executable cache misses on source and resolution changes", async () => { diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index ffbf939b3..3118c30b1 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -388,7 +388,6 @@ export async function readEarlyExecutableCache( if (!(await fileMatches(paths.cPath, stamp.files.translationUnit.digest))) { await installBytes(translationUnit, paths.cPath); } - await rm(paths.staleCPath, { force: true }); if ( ir !== null && stamp.files.ir !== null && !(await fileMatches(paths.irPath, stamp.files.ir.digest)) diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 9773f0319..488744335 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -195,9 +195,7 @@ export interface CompileBaseOptions { outPath: string; /** Where generated intermediates and compatibility side artifacts land. */ outDir: string; - /** True only when outPath was selected by scriptc's default-path policy. - * This authorizes cleanup of stale generated siblings; explicit paths must - * leave neighboring caller-owned files untouched. */ + /** @deprecated This option no longer controls output cleanup; sibling artifacts are retained. */ defaultOutputPath?: boolean; /** Compatibility-only additive IR side artifact for executable builds. * The CLI's deprecated --emit-ir flag supplies this option. */ @@ -1424,10 +1422,6 @@ async function compileTracked( throw new InternalCompilerError("executable cache hit without executable cache options"); } const executableCacheOptions = earlyCacheOptions; - if (!opts.emitIr) { - const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); - await rm(join(opts.outDir, `${stem}.ir.json`), { force: true }); - } // Route/proof metadata is independently evictable. A full-compiler // fallback that still finds the validated payload repairs that lightweight // index so the next identical CLI invocation can avoid this module graph. @@ -1615,42 +1609,17 @@ async function compileTracked( ir: join(opts.outDir, `${stem}.ir.json`), c: join(opts.outDir, `${stem}.c`), llvm: join(opts.outDir, `${stem}.ll`), - asm: join(opts.outDir, `${stem}.s`), - obj: join(opts.outDir, `${stem}.o`), } as const; - const defaultExecutablePaths = [ - join(opts.outDir, stem), - join(opts.outDir, `${stem}.exe`), - join(opts.outDir, `${stem}.wasm`), - ]; - const removeStaleSourceArtifacts = async (keep: readonly string[]): Promise => { - const kept = new Set(keep.map((path) => resolve(path))); - const candidates = outputKind === "exe" - // Executable builds can generate only these compatibility/translation - // unit siblings. Assembly and object outputs are independent primary - // artifacts, so an executable build must never claim or delete them. - ? [defaultSourcePaths.ir, defaultSourcePaths.c, defaultSourcePaths.llvm] - : opts.defaultOutputPath === true - ? [...Object.values(defaultSourcePaths), ...defaultExecutablePaths] - : []; - await Promise.all( - candidates - .filter((path) => !kept.has(resolve(path))) - .map((path) => rm(path, { force: true })), - ); - }; if (outputKind === "ir") { await mkdir(dirname(opts.outPath), { recursive: true }); await writeFile(opts.outPath, serializeModule(lowered.module)); - await removeStaleSourceArtifacts([opts.outPath]); return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; } if (outputKind === "c") { await mkdir(dirname(opts.outPath), { recursive: true }); await writeFile(opts.outPath, emitCModule(lowered.module, entryText)); - await removeStaleSourceArtifacts([opts.outPath]); return { ok: true, artifact: { kind: "c", path: opts.outPath } }; } @@ -1688,7 +1657,6 @@ async function compileTracked( }; } } - await removeStaleSourceArtifacts([opts.outPath]); if (outputKind === "obj" && opts.nativeLinkInfo === true) { const target = nativeCodegenTarget(); if (target === null) { @@ -1753,19 +1721,11 @@ async function compileTracked( if (backend === "c") { await writeFile(cPath, emitCModule(lowered.module!, entryText)); } - // Kept-TU honesty: outDir persists across builds (the CLI's .scriptc/), - // so a lane change would leave the PREVIOUS lane's TU beside the fresh - // one — remove the loser so the surviving TU is always the one the - // binary below was linked from. let irPath: string | undefined; if (opts.emitIr) { irPath = defaultSourcePaths.ir; await writeFile(irPath, serializeModule(lowered.module)); } - await removeStaleSourceArtifacts([ - cPath, - ...(irPath === undefined ? [] : [irPath]), - ]); const nativeFeatures = executableNativeFeatures( lowered.module, @@ -2442,7 +2402,6 @@ async function emitSemanticLibraryHit( await writeFile(cPath, translationUnit); } timing("semantic-tu-restore", { output_bytes: Buffer.byteLength(translationUnit) }); - await rm(join(opts.outDir, `${stem}.lib.${profile.emission === "llvm" ? "c" : "ll"}`), { force: true }); let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); @@ -2911,7 +2870,6 @@ async function compileLibraryTracked( cPath = join(opts.outDir, `${stem}.lib.c`); await writeFile(cPath, emitCModule(mod, entryText)); } - await rm(join(opts.outDir, `${stem}.lib.${profile.emission === "llvm" ? "c" : "ll"}`), { force: true }); let irPath: string | undefined; if (opts.emitIr) { diff --git a/packages/compiler/src/library/cache-primitives.ts b/packages/compiler/src/library/cache-primitives.ts index 21b00737d..65ee71f64 100644 --- a/packages/compiler/src/library/cache-primitives.ts +++ b/packages/compiler/src/library/cache-primitives.ts @@ -14,7 +14,7 @@ interface CacheOutputOptions { export interface CacheOutputPaths { cPath: string; - staleCPath: string; + alternateCPath: string; irPath: string; } @@ -55,7 +55,7 @@ export function outputPaths( const stem = basename(options.entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "") + stemSuffix; return { cPath: join(options.outDir, `${stem}.${backend === "llvm" ? "ll" : "c"}`), - staleCPath: join(options.outDir, `${stem}.${backend === "llvm" ? "c" : "ll"}`), + alternateCPath: join(options.outDir, `${stem}.${backend === "llvm" ? "c" : "ll"}`), irPath: join(options.outDir, `${stem}.ir.json`), }; } @@ -69,8 +69,8 @@ export function frontendOutputExclusions( const paths = outputPaths(options, backend, stemSuffix); const outputArtifacts = [ paths.cPath, - paths.staleCPath, - ...(options.emitIr ? [paths.irPath] : []), + paths.alternateCPath, + paths.irPath, ...additionalPaths, ].map((path) => resolve(path)); const outputDirectories = new Set(); diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index 0c124caa9..d1e54b7dc 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -90,8 +90,8 @@ test("early library cache restores generated artifacts and metadata", async () = frontend: tracker.snapshot(), }); await Promise.all([rm(f.cPath), rm(f.irPath), rm(f.sidecarPath)]); - const staleCPath = join(f.options.outDir, "entry.lib.c"); - await writeFile(staleCPath, "/* stale c backend */\n"); + const alternateCPath = join(f.options.outDir, "entry.lib.c"); + await writeFile(alternateCPath, "/* saved c backend */\n"); const hit = await readEarlyLibraryCache(f.root, f.options, null); expect(hit).not.toBeNull(); @@ -99,7 +99,7 @@ test("early library cache restores generated artifacts and metadata", async () = expect(await readFile(f.cPath, "utf8")).toBe("; generated llvm\n"); expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); expect(await readFile(f.sidecarPath, "utf8")).toContain("contract"); - await expect(readFile(staleCPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(alternateCPath, "utf8")).toBe("/* saved c backend */\n"); }); test("early library cache publishes after creating a fresh output directory", async () => { diff --git a/packages/compiler/src/library/library-cache.ts b/packages/compiler/src/library/library-cache.ts index 4b7bd7c2f..ef6ce5545 100644 --- a/packages/compiler/src/library/library-cache.ts +++ b/packages/compiler/src/library/library-cache.ts @@ -259,7 +259,6 @@ export async function readEarlyLibraryCache( const paths = outputPaths(options, stamp.native.backend); await installBytes(translationUnit, paths.cPath); - await rm(paths.staleCPath, { force: true }); if (ir !== null) await installBytes(ir, paths.irPath); let sidecarPath: string | undefined; if (sidecar !== null) { diff --git a/packages/compiler/test/source-output.test.ts b/packages/compiler/test/source-output.test.ts index 7e61f7cf0..157dafc5c 100644 --- a/packages/compiler/test/source-output.test.ts +++ b/packages/compiler/test/source-output.test.ts @@ -55,10 +55,12 @@ test("C and LLVM are exact primary artifacts and never create an executable", as expect((await readdir(outDir)).sort()).toEqual(["exact.llvm-output", "exact.output"]); }); -test("switching default source output kinds removes stale generated siblings", async () => { +test("switching default source output kinds preserves earlier generated siblings", async () => { const { entry, outDir } = await fixture(); await mkdir(outDir, { recursive: true }); - await writeFile(join(outDir, process.platform === "win32" ? "main.exe" : "main"), "stale executable"); + const executable = process.platform === "win32" ? "main.exe" : "main"; + await writeFile(join(outDir, executable), "saved executable"); + const artifacts = [executable]; for (const [kind, name] of [ ["c", "main.c"], ["llvm", "main.ll"], @@ -71,8 +73,10 @@ test("switching default source output kinds removes stale generated siblings", a defaultOutputPath: true, }); if (!result.ok) throw new Error(`${kind} emission failed`); - expect(await readdir(outDir)).toEqual([name]); + artifacts.push(name); + expect((await readdir(outDir)).sort()).toEqual([...artifacts].sort()); } + expect(await readFile(join(outDir, executable), "utf8")).toBe("saved executable"); }); test("an explicit source path never deletes same-stem sibling files", async () => {