Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions packages/compiler/src/backend/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "";
}
}

Expand Down Expand Up @@ -3728,22 +3740,55 @@ export async function compileC(opts: CcOptions): Promise<void> {
const programSourceExtension = opts.cPath.endsWith(".ll") ? ".ll" : ".c";
const ccName = driver.argv.join(" ");
const runClang = async (args: string[]): Promise<void> => {
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
? "This build includes native FFI link inputs. Check that every symbol and system library exists, " +
"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}` : " <empty>"}`,
...(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 },
);
}
};
Expand Down
53 changes: 51 additions & 2 deletions packages/compiler/test/cc-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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: <empty>");
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");
Expand Down