fix(windows): support GUI executables

- Add Windows subsystem selection across executable link paths and cache keys.
- Verify PE subsystem output and add Windows CI coverage.

Fixes #259

Co-authored-by: Mateus Mamede Lage <23098414+mmamedel@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-09-22 19:05:42 -05:00
co-authored by Mateus Mamede Lage
parent 2df164ab99
commit 26c625f3a9
16 changed files with 255 additions and 14 deletions
+4
View File
@@ -350,6 +350,10 @@ jobs:
if ($actual -ne "hello world") {
throw "unexpected scriptc output: $actual"
}
- name: Windows PE subsystem selection
env:
SCRIPTC_NO_CACHE: "0"
run: pnpm exec vitest run packages/cli/test/windows-subsystem.test.ts
- name: Windows CA-store differential
shell: pwsh
run: |
+3
View File
@@ -104,6 +104,9 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
<dt><code>--backend &lt;c|llvm&gt;</code></dt>
<dd>Code generator: <code>llvm</code> (default) or <code>c</code> (the readable debugging backend). On supported targets, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>.</dd>
<dt><code>--windows-subsystem &lt;console|gui&gt;</code></dt>
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows, including <code>--from-c</code>.</dd>
<dt><code>--npm-static &lt;pkg[,pkg…]|auto&gt;</code></dt>
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>
+2
View File
@@ -35,6 +35,8 @@ fib.exe: PE32+ executable (console) x86-64, for MS Windows
The full corpus runs in a differential lane against Windows Node, including `--dynamic` programs and `child_process`. The Windows fixture lanes also exercise real loopback traffic through `net`, `http`, `https`, `tls`, `http2`, `dgram`, and `dns`, plus the native `fetch` implementation (including redirects, streaming bodies, compression, cancellation, and proxy handling).
Windows executable builds use the console subsystem by default. Set <code>--windows-subsystem=gui</code> for an application that owns its own window so Windows does not open an extra console window. This selects the PE subsystem at link time and works with either backend.
### iOS and Android (arm64, library mode)
Three mobile triples build **library-mode static archives** for an embedding app to link — a mobile app is the executable, so `scriptc build` without `--lib` rejects these targets with `SC3002`:
+16 -3
View File
@@ -71,7 +71,7 @@ async function main(): Promise<number> {
const [command, inputArg] = positionals;
if (command === "cache") {
if (inputArg !== "warm") fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`);
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["from-c"] || values.ffi !== undefined || values.profile !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-c"]) {
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["from-c"] || values.ffi !== undefined || values.profile !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-c"]) {
fail(`scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`);
}
const optimization = values.optimization;
@@ -119,9 +119,9 @@ async function main(): Promise<number> {
if (inputArg) {
fail("scriptc build --lib takes no input positional: the profile names the entry module");
}
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.ffi !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.ffi !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
fail(
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
);
}
const profilePath = resolve(profileArg);
@@ -201,6 +201,13 @@ async function main(): Promise<number> {
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") {
fail(`unknown optimization "${optimization}" (supported: release, dev)\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 output = command === "coverage"
? null
: resolveOutputOptions(command, {
@@ -213,6 +220,7 @@ async function main(): Promise<number> {
keepC: values["keep-c"],
sanitize: values.sanitize,
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
});
if (output !== null && !output.ok) fail(`${output.message}\n\n${USAGE}`);
@@ -258,6 +266,9 @@ async function main(): Promise<number> {
}
if (output === null || !output.ok) throw new Error("internal output-option state");
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);
// SCRIPTC_CC remains a migration escape hatch for explicit C, sanitizer,
@@ -284,6 +295,7 @@ async function main(): Promise<number> {
sanitize: values.sanitize,
dynamic: values.dynamic,
...(optimization !== undefined ? { optimization } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
});
return outPath;
}
@@ -297,6 +309,7 @@ async function main(): Promise<number> {
dynamic: values.dynamic,
...(backend !== undefined ? { backend } : {}),
...(optimization !== undefined ? { optimization } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
+4
View File
@@ -9,6 +9,7 @@ export interface OutputOptionValues {
keepC: boolean;
sanitize: boolean;
optimization?: string;
windowsSubsystem?: string;
ffi?: string;
}
@@ -58,6 +59,9 @@ export function resolveOutputOptions(
if (values.fromC && emit !== "exe") {
return { ok: false, message: `--from-c only supports --emit=exe` };
}
if (values.windowsSubsystem !== undefined && emit !== "exe") {
return { ok: false, message: `--windows-subsystem is only supported with --emit=exe` };
}
if (emit === "ir" && backend !== undefined) {
return { ok: false, message: `--emit=ir cannot be combined with --backend; IR is emitted before backend selection` };
}
+4
View File
@@ -35,6 +35,9 @@ Options:
native optimization posture (default: release/-O2). dev
uses -O0 and stable cached LLVM object shards for faster
edits of large programs
--windows-subsystem <console|gui>
Windows executable subsystem (default: console). gui
prevents Windows from opening a console window
--from-c treat input as a C (or .ll) file (toolchain plumbing/debugging)
--keep-c keep the generated program TU next to the executable
(default; the .ll — or the .c under --backend=c or
@@ -74,6 +77,7 @@ export const CLI_OPTIONS = {
print: { type: "string" },
backend: { type: "string" },
optimization: { type: "string" },
"windows-subsystem": { type: "string" },
"from-c": { type: "boolean", default: false },
"keep-c": { type: "boolean", default: true },
"emit-ir": { type: "boolean", default: false },
+9
View File
@@ -46,6 +46,8 @@ describe("output option compatibility", () => {
[{ emit: "llvm", emitIr: true }, /emit-ir/],
[{ emit: "ir", emitIr: true }, /same output/],
[{ backend: "wat" }, /unknown backend/],
[{ emit: "obj", windowsSubsystem: "gui" }, /windows-subsystem/],
[{ emit: "llvm", windowsSubsystem: "gui" }, /windows-subsystem/],
] as const)("rejects %j", (override, message) => {
const result = resolveOutputOptions("build", { ...BASE, ...override });
expect(result).toEqual({ ok: false, message: expect.stringMatching(message) });
@@ -89,4 +91,11 @@ describe("output option compatibility", () => {
outputKind: "llvm",
});
});
test("executable outputs accept a Windows subsystem selection", () => {
expect(resolveOutputOptions("build", { ...BASE, windowsSubsystem: "gui" })).toMatchObject({
ok: true,
outputKind: "exe",
});
});
});
+106
View File
@@ -0,0 +1,106 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
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";
import { afterEach, expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
const dirs: string[] = [];
afterEach(async () => {
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
async function fixture() {
const dir = await mkdtemp(join(tmpdir(), "scriptc-windows-subsystem-"));
dirs.push(dir);
const entry = join(dir, "app.ts");
await writeFile(entry, 'console.log("subsystem");\n');
return { dir, entry };
}
function cli(args: string[], env: NodeJS.ProcessEnv = process.env) {
return execFileAsync(process.execPath, ["--import", tsxLoader, cliEntry, ...args], {
env,
maxBuffer: 4 * 1024 * 1024,
});
}
async function peSubsystem(path: string): Promise<number> {
const bytes = await readFile(path);
expect(bytes.toString("ascii", 0, 2)).toBe("MZ");
const peOffset = bytes.readUInt32LE(0x3c);
expect(bytes.toString("ascii", peOffset, peOffset + 4)).toBe("PE\0\0");
const optional = peOffset + 24;
expect(bytes.readUInt16LE(optional)).toBe(0x20b);
return bytes.readUInt16LE(optional + 68);
}
test("rejects the subsystem option outside Windows executable builds", async () => {
const { dir, entry } = await fixture();
const invalid = [
["build", entry, "--windows-subsystem=other"],
["build", entry, "--emit=llvm", "--windows-subsystem=gui"],
["build", entry, "--emit=obj", "--windows-subsystem=gui"],
["coverage", entry, "--windows-subsystem=gui"],
];
for (const args of invalid) {
await expect(cli(args)).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("--windows-subsystem"),
});
}
await expect(cli(["build", entry, "--windows-subsystem=gui"], {
...process.env,
SCRIPTC_TARGET: "wasm32-wasi",
})).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("requires a Windows executable target"),
});
await expect(readFile(join(dir, ".scriptc", "app.wasm"))).rejects.toMatchObject({ code: "ENOENT" });
});
test.skipIf(process.platform !== "win32" && process.env["SCRIPTC_WIN_SUBSYSTEM"] !== "1")(
"builds console and GUI PE executables without cross-subsystem cache hits",
async () => {
const { dir, entry } = await fixture();
const env = process.platform === "win32"
? { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") }
: {
...process.env,
SCRIPTC_CC: "zigcc",
SCRIPTC_TARGET: "x86_64-windows-gnu",
SCRIPTC_CACHE_DIR: join(dir, "cache"),
ZIG_GLOBAL_CACHE_DIR: join(dir, "zig-cache"),
};
const llvmOut = join(dir, "llvm.exe");
const cOut = join(dir, "c.exe");
const build = async (backend: "llvm" | "c", out: string, subsystem?: "console" | "gui") => {
await cli([
"build", entry, `--backend=${backend}`, "-o", out,
...(subsystem === undefined ? [] : [`--windows-subsystem=${subsystem}`]),
], env);
return peSubsystem(out);
};
expect(await build("llvm", llvmOut)).toBe(3);
expect(await build("llvm", llvmOut, "gui")).toBe(2);
expect(await build("llvm", llvmOut)).toBe(3);
expect(await build("llvm", llvmOut, "console")).toBe(3);
expect(await build("c", cOut, "gui")).toBe(2);
const cSource = join(dir, "from-c.c");
const fromCOut = join(dir, "from-c.exe");
await writeFile(cSource, "int main(void) { return 0; }\n");
await cli(["build", cSource, "--from-c", "--windows-subsystem=gui", "-o", fromCOut], env);
expect(await peSubsystem(fromCOut)).toBe(2);
if (process.platform === "win32") {
expect((await execFileAsync(cOut)).stdout.trim()).toBe("subsystem");
}
},
180_000,
);
@@ -12,7 +12,9 @@ import type { NativeArtifactDependency } from "./native-toolchain.js";
import { loadRuntimePack, type RuntimePackSelection } from "./runtime-pack.js";
import {
executableOptimizationLinkerArgs,
windowsSubsystemLinkerArgs,
type NativeTargetSpec,
type WindowsSubsystem,
} from "./targets.js";
export interface NativeLinkPlan {
@@ -36,6 +38,7 @@ export async function createNativeLinkPlan(options: {
features: NativeLinkFeatures;
ffi: FfiProfile | null;
optimization: "release" | "dev";
windowsSubsystem?: WindowsSubsystem;
programObjectDependencies?: readonly NativeArtifactDependency[];
env?: NodeJS.ProcessEnv;
resolver?: (specifier: string) => string;
@@ -59,6 +62,7 @@ export async function createNativeLinkPlan(options: {
index > 0 && args[index - 1] === "-target" ? options.target.linkerTargetTriple : arg
),
...executableOptimizationLinkerArgs(options.target.platform, options.optimization),
...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem),
],
dependencyPaths: [
...runtimePack.dependencyPaths,
@@ -10,7 +10,7 @@ import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
import { executableOptimizationLinkerArgs } from "./targets.js";
import { executableOptimizationLinkerArgs, windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./targets.js";
import {
createVendorArchives,
MBEDTLS_VERSION,
@@ -285,6 +285,8 @@ export interface CcOptions {
* executable lane; dev selects -O0 and may compile a caller-provided LLVM
* shard set into independently cached objects before the final link. */
optimization?: "release" | "dev";
/** PE executable subsystem; omitted and console use the driver default. */
windowsSubsystem?: WindowsSubsystem;
/** Optional equivalent LLVM modules for dev compilation. Unsupported
* targets or merge failures fall back to the canonical cPath TU. */
programShards?: readonly { name: string; source: string }[];
@@ -4053,12 +4055,14 @@ async function compileCInternal(
? EXECUTABLE_RUNTIME_SOURCES.filter((source) => source !== "scr_child.c")
: EXECUTABLE_RUNTIME_SOURCES;
const executableSectionFlags = executableSectionEliminationFlags(targetPlatform(driver));
const windowsSubsystemArgs = windowsSubsystemLinkerArgs(targetPlatform(driver), opts.windowsSubsystem);
const executableLinkFlags = [
...executableSectionFlags.link,
...executableOptimizationLinkerArgs(
targetPlatform(driver),
optimization,
),
...windowsSubsystemArgs,
];
// scr_async.c submits callback-style filesystem work to a native worker.
// POSIX drivers need the thread compile/link mode; win32 uses CreateThread.
@@ -21,7 +21,7 @@ import {
platformLinkerSupportsPersistentCache,
resolvePlatformLinker,
} from "./linker.js";
import { MACOS_ARM64_TARGET } from "./targets.js";
import { MACOS_ARM64_TARGET, WINDOWS_X64_MSVC_TARGET, type NativeTargetSpec } from "./targets.js";
const VERSION = compilerReleaseVersion();
@@ -58,11 +58,11 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
};
async function fixture() {
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-unit-"));
const packagePath = join(root, "package.json");
await writeFile(packagePath, JSON.stringify({
name: "@scriptc/runtime-darwin-arm64",
name: target.runtimePackPackage,
version: VERSION,
}));
const artifact = async (path: string, bytes: string) => {
@@ -93,20 +93,20 @@ async function fixture() {
const manifest: RuntimePackManifest = {
schema: "scriptc.runtime-pack.v1",
format: 1,
package: "@scriptc/runtime-darwin-arm64",
package: target.runtimePackPackage,
version: VERSION,
target: {
name: "macos-arm64",
llvm_triple: "arm64-apple-macosx14.0.0",
architecture: "arm64",
object_format: "macho",
minimum_os: "14.0",
name: target.name,
llvm_triple: target.llvmTriple,
architecture: target.architecture,
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 1, marker: "scr_runtime_abi_v1" },
compiler: {
command: "clang",
identity: "fixture clang",
target: "arm64-apple-macosx14.0.0",
target: target.llvmTriple,
},
macros: {
executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"],
@@ -194,6 +194,25 @@ describe("runtime pack manifests", () => {
expect(plan.driverFlags).toContain("-Wl,-dead_strip");
});
test("Windows runtime-pack links select the GUI subsystem only when requested", async () => {
const { packagePath, root } = await fixture(WINDOWS_X64_MSVC_TARGET);
const options = {
target: WINDOWS_X64_MSVC_TARGET,
programObject: join(root, "program.obj"),
outPath: join(root, "program.exe"),
features: BASE,
ffi: null,
optimization: "release" as const,
resolver: () => packagePath,
};
const defaultPlan = await createNativeLinkPlan(options);
const consolePlan = await createNativeLinkPlan({ ...options, windowsSubsystem: "console" });
const guiPlan = await createNativeLinkPlan({ ...options, windowsSubsystem: "gui" });
expect(defaultPlan.driverFlags).not.toContain("-Wl,--subsystem,windows");
expect(consolePlan.driverFlags).toEqual(defaultPlan.driverFlags);
expect(guiPlan.driverFlags).toEqual([...defaultPlan.driverFlags, "-Wl,--subsystem,windows"]);
});
test("selection chooses the most-specific variant and feature archive", async () => {
const { packagePath } = await fixture();
const resolver = () => packagePath;
@@ -11,6 +11,7 @@ import {
executableOptimizationLinkerArgs,
nativeCodegenTarget,
nativeCodegenTargetRefusal,
windowsSubsystemLinkerArgs,
} from "./targets.js";
describe("native code-generation targets", () => {
@@ -66,4 +67,12 @@ describe("native code-generation targets", () => {
expect(executableOptimizationLinkerArgs("wasi", "dev")).toEqual([]);
expect(executableOptimizationLinkerArgs("linux", "release")).toEqual([]);
});
test("selects the PE GUI subsystem without changing default console links", () => {
expect(windowsSubsystemLinkerArgs("win32", undefined)).toEqual([]);
expect(windowsSubsystemLinkerArgs("win32", "console")).toEqual([]);
expect(windowsSubsystemLinkerArgs("win32", "gui")).toEqual(["-Wl,--subsystem,windows"]);
expect(() => windowsSubsystemLinkerArgs("linux", "gui")).toThrow("Windows executable target");
expect(() => windowsSubsystemLinkerArgs("win32", "other" as "gui")).toThrow("unknown Windows subsystem");
});
});
+18
View File
@@ -23,6 +23,24 @@ export type NativeTargetName =
export type NativeObjectFormat = "macho" | "elf" | "coff" | "wasm";
export type NativeTargetPlatform = "darwin" | "linux" | "win32" | "wasi";
export type WindowsSubsystem = "console" | "gui";
/** Select the PE subsystem through either supported compiler-driver route.
* An omitted or explicit console selection retains the driver's default. */
export function windowsSubsystemLinkerArgs(
platform: string,
subsystem: WindowsSubsystem | undefined,
): string[] {
if (subsystem === undefined) return [];
if (subsystem !== "console" && subsystem !== "gui") {
throw new Error(`unknown Windows subsystem '${String(subsystem)}' (supported: console, gui)`);
}
if (platform !== "win32") {
throw new Error("--windows-subsystem requires a Windows executable target");
}
return subsystem === "gui" ? ["-Wl,--subsystem,windows"] : [];
}
/** Linker flags that belong to an optimization posture rather than the
* target ABI. Zig's WASI runtime objects carry DWARF custom sections even
* when compiled with -O2; release executables discard that non-runtime
@@ -85,6 +85,8 @@ export interface EarlyExecutableCacheOptions {
backend: "auto" | "c" | "llvm";
/** Omitted is the historical release posture and preserves v1 keys. */
optimization?: "dev";
/** Omitted for the default Windows console subsystem. */
windowsSubsystem?: "gui";
npmStatic: readonly string[] | "auto" | null;
/** Raw manifest identity: path and bytes. Native archives remain under the
* stricter native cache's independent dependency validation. */
@@ -186,6 +188,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
options.dynamic ? "dynamic" : "static",
options.backend,
...(options.optimization === "dev" ? ["optimization-dev"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
options.npmStatic === null
? "<npm-static-off>"
: options.npmStatic === "auto"
@@ -213,6 +216,7 @@ function routeKey(options: EarlyExecutableRouteOptions): string {
.update(options.dynamic ? "dynamic" : "static").update("\0")
.update(options.backend).update("\0");
if (options.optimization === "dev") hash.update("optimization-dev\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
hash
.update(options.npmStatic === null
? "<npm-static-off>"
+27
View File
@@ -21,6 +21,7 @@ import { emitLlvmModule, LlvmUnsupportedError } from "./backend/llvm/emitter.js"
import { emitNativeArtifact, NativeCodegenError } from "./backend/native-codegen.js";
import { privateSiblingPath } from "./backend/build-cache.js";
import { nativeCodegenTarget, nativeCodegenTargetRefusal } from "./backend/targets.js";
import { windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./backend/targets.js";
import { createNativeLinkInfo, type NativeLinkInfo } from "./backend/native-link-info.js";
import { RuntimePackError } from "./backend/runtime-pack.js";
import { createNativeLinkPlan } from "./backend/link-plan.js";
@@ -80,6 +81,7 @@ export {
RUNTIME_ABI_VERSION,
} from "./backend/runtime-abi.js";
export type { NativeLinkInfo, NativeLinkFeatures } from "./backend/native-link-info.js";
export type { WindowsSubsystem } from "./backend/targets.js";
export { InternalCompilerError } from "./errors.js";
export {
@@ -223,6 +225,9 @@ export interface CompileBaseOptions {
/** Native optimization posture. Release is the shipped -O2 default; dev
* uses -O0 and stable multi-TU object caching for large LLVM programs. */
optimization?: "release" | "dev";
/** Windows PE executable subsystem. Console is the default; GUI suppresses
* automatic console-window creation. Only valid for Windows executables. */
windowsSubsystem?: WindowsSubsystem;
/** --npm-static: package names whose shipped, unminified JS compiles
* STATICALLY as program modules (inference types the bodies; statements
* the lowering cannot prove become runtime fences). "auto" opts in every
@@ -1073,6 +1078,7 @@ async function compileExecutableNative(
outPath: string,
sanitize: boolean,
ffi: FfiProfile | null,
windowsSubsystem?: WindowsSubsystem,
programSplit: ReturnType<typeof splitLlvmProgram> = null,
programObjectDependencies: readonly NativeArtifactDependency[] = [],
onArtifactReady?: NonNullable<Parameters<typeof compileExternalC>[0]["onArtifactReady"]>,
@@ -1089,6 +1095,7 @@ async function compileExecutableNative(
features,
ffi,
optimization: features.optimization ?? "release",
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
programObjectDependencies,
});
const cacheableLinker =
@@ -1125,6 +1132,7 @@ async function compileExecutableNative(
outPath,
cacheIdentity: "scriptc-generated-v1",
...(features.optimization === "dev" ? { optimization: "dev" as const } : {}),
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(effectiveProgramSplit === null
? {}
: {
@@ -1230,6 +1238,13 @@ async function compileTracked(
): Promise<CompileRequestResult> {
entryPath = resolve(entryPath);
const outputKind = opts.outputKind ?? "exe";
if (opts.windowsSubsystem !== undefined && outputKind !== "exe") {
return {
ok: false,
diagnostics: [nativeCodegenDiag("SC3002", "--windows-subsystem is only supported for executable output", entryPath)],
sourceTexts: new Map(),
};
}
if (opts.nativeLinkInfo === true && outputKind !== "obj") {
return {
ok: false,
@@ -1318,6 +1333,15 @@ async function compileTracked(
}
}
}
try {
windowsSubsystemLinkerArgs(buildPlatform, opts.windowsSubsystem);
} catch (err) {
return {
ok: false,
diagnostics: [nativeCodegenDiag("SC3002", err instanceof Error ? err.message : String(err), entryPath)],
sourceTexts: new Map(),
};
}
// Mobile triples are library-mode targets: the archive an embedding app
// links is the artifact, and only the library-admissible runtime surface
// is verified on those device classes. The executable lane refuses before
@@ -1365,6 +1389,7 @@ async function compileTracked(
dynamic: opts.dynamic ?? false,
backend: opts.backend ?? "auto",
...(opts.optimization === "dev" ? { optimization: "dev" as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
npmStatic: opts.npmStatic ?? null,
ffiProfile:
opts.ffiProfilePath === undefined || ffiProfileBytes === null
@@ -1466,6 +1491,7 @@ async function compileTracked(
opts.outPath,
opts.sanitize ?? false,
ffi,
opts.windowsSubsystem,
null,
nativeProgramObject?.dependencies,
opts.nativeProgramObject === true ? undefined : async ({ dependencies }) => {
@@ -1788,6 +1814,7 @@ async function compileTracked(
opts.outPath,
opts.sanitize ?? false,
ffi,
opts.windowsSubsystem,
programSplit,
nativeProgramObject?.dependencies,
opts.nativeProgramObject === true ? undefined : async ({ dependencies }) => {
@@ -28,6 +28,17 @@ test("serialized IR is the primary artifact and round-trips through validation",
expect(validateModule(module)).toEqual([]);
});
test("Windows subsystem selection refuses non-executable compiler output", async () => {
const { entry, outDir } = await fixture();
const outPath = join(outDir, "main.ir.json");
const result = await compile(entry, { outDir, outPath, outputKind: "ir", windowsSubsystem: "gui" });
expect(result).toMatchObject({
ok: false,
diagnostics: [{ code: "SC3002", message: expect.stringContaining("only supported for executable output") }],
});
await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" });
});
test("C and LLVM are exact primary artifacts and never create an executable", async () => {
const { entry, outDir } = await fixture();
const cPath = join(outDir, "exact.output");