diff --git a/packages/compiler/src/backend/cc.ts b/packages/compiler/src/backend/cc.ts index 94a00daa2..28d90be3d 100644 --- a/packages/compiler/src/backend/cc.ts +++ b/packages/compiler/src/backend/cc.ts @@ -371,13 +371,25 @@ export interface CcOptions { * when an outbound FFI profile is active and turns user-controlled native * link failures into SC5004. */ export class CcCompileError extends Error { + readonly command: string; + readonly exitCode: number | null; + readonly stdout: string; + constructor( readonly driver: string, readonly stderr: string, message: string, + details?: { + command: string; + exitCode: number | null; + stdout: string; + }, ) { super(message); this.name = "CcCompileError"; + this.command = details?.command ?? driver; + this.exitCode = details?.exitCode ?? null; + this.stdout = details?.stdout ?? ""; } } @@ -3728,10 +3740,32 @@ export async function compileC(opts: CcOptions): Promise { const programSourceExtension = opts.cPath.endsWith(".ll") ? ".ll" : ".c"; const ccName = driver.argv.join(" "); const runClang = async (args: string[]): Promise => { + const executable = driver.argv[0] ?? "clang"; + const argv = [...driver.argv.slice(1), ...args]; try { - await execFileAsync(driver.argv[0] ?? "clang", [...driver.argv.slice(1), ...args]); + await execFileAsync(executable, argv); } catch (err) { - const stderr = (err as { stderr?: string }).stderr ?? String(err); + const failure = err as { + cmd?: unknown; + code?: unknown; + stderr?: unknown; + stdout?: unknown; + }; + const command = + typeof failure.cmd === "string" && failure.cmd.length > 0 + ? failure.cmd + : [executable, ...argv].join(" "); + const exitCode = typeof failure.code === "number" ? failure.code : null; + const stderr = typeof failure.stderr === "string" + ? failure.stderr + : Buffer.isBuffer(failure.stderr) + ? failure.stderr.toString() + : ""; + const stdout = typeof failure.stdout === "string" + ? failure.stdout + : Buffer.isBuffer(failure.stdout) + ? failure.stdout.toString() + : ""; const guidance = (opts.linkInputs?.length ?? 0) > 0 || (opts.systemLibraries?.length ?? 0) > 0 @@ -3739,11 +3773,22 @@ export async function compileC(opts: CcOptions): Promise { "that archive/object ordering is correct, and that each input matches the selected target." : `This is a scriptc bug (generated C should always compile) unless ` + `${ccName} itself is missing/broken.`; + const processError = String(err); + const output = [ + `Command: ${command}`, + `Exit code: ${exitCode ?? "unavailable"}`, + `Compiler stderr:${stderr.length > 0 ? `\n${stderr}` : " "}`, + ...(stdout.length > 0 ? [`Compiler stdout:\n${stdout}`] : []), + ...(stderr.length === 0 && stdout.length === 0 + ? [`Process error: ${processError}`] + : []), + ].join("\n"); throw new CcCompileError( ccName, stderr, `${ccName} failed compiling ${opts.cPath}.\n` + - `${guidance}\n\n${stderr}`, + `${guidance}\n\n${output}`, + { command, exitCode, stdout }, ); } }; diff --git a/packages/compiler/test/cc-driver.test.ts b/packages/compiler/test/cc-driver.test.ts index 995dd39a2..33c7e4fea 100644 --- a/packages/compiler/test/cc-driver.test.ts +++ b/packages/compiler/test/cc-driver.test.ts @@ -17,10 +17,10 @@ import { execFile, execFileSync } from "node:child_process"; import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { promisify } from "node:util"; import { describe, expect, test } from "vitest"; -import { compileC, resolveCc, runtimeSrcDir } from "../src/backend/cc.js"; +import { CcCompileError, compileC, resolveCc, runtimeSrcDir } from "../src/backend/cc.js"; const execFileAsync = promisify(execFile); @@ -174,6 +174,55 @@ test("host-native clang static build compiles the runtime and runs", async () => expect(stdout).toBe("clang says hi\n"); }); +test.skipIf(process.platform === "win32")( + "compiler failures retain the command, exit code, and captured output", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scr-cc-failure-")); + const binDir = join(dir, "bin"); + const fakeClang = join(binDir, "clang"); + const cPath = join(dir, "program.c"); + const outPath = join(dir, "program"); + await mkdir(binDir); + await writeFile( + fakeClang, + '#!/bin/sh\nprintf "compiler stdout marker\\n"\nexit 23\n', + ); + await chmod(fakeClang, 0o755); + await writeFile(cPath, "int main(void) { return 0; }\n"); + + const previousPath = process.env["PATH"]; + const previousNoCache = process.env["SCRIPTC_NO_CACHE"]; + process.env["PATH"] = `${binDir}${delimiter}${previousPath ?? ""}`; + process.env["SCRIPTC_NO_CACHE"] = "1"; + try { + let failure: unknown; + try { + await compileC({ cPath, outPath }); + } catch (error) { + failure = error; + } + + expect(failure).toBeInstanceOf(CcCompileError); + if (!(failure instanceof CcCompileError)) throw failure; + expect(failure.command).toContain(`clang`); + expect(failure.command).toContain(cPath); + expect(failure.command).toContain(outPath); + expect(failure.exitCode).toBe(23); + expect(failure.stderr).toBe(""); + expect(failure.stdout).toBe("compiler stdout marker\n"); + expect(failure.message).toContain(`Command: ${failure.command}`); + expect(failure.message).toContain("Exit code: 23"); + expect(failure.message).toContain("Compiler stderr: "); + expect(failure.message).toContain("Compiler stdout:\ncompiler stdout marker"); + } finally { + if (previousPath === undefined) delete process.env["PATH"]; + else process.env["PATH"] = previousPath; + if (previousNoCache === undefined) delete process.env["SCRIPTC_NO_CACHE"]; + else process.env["SCRIPTC_NO_CACHE"] = previousNoCache; + } + }, +); + test("host-native clang static build links native fetch after zlib inputs", async () => { const dir = await mkdtemp(join(tmpdir(), "scr-host-fetch-clang-")); const cPath = join(dir, "program.c");