Fix scriptc run on Windows

- Normalize Windows paths passed through the TS7 virtual filesystem.
- Emit and launch .exe outputs by default on Windows.
- Add targeted regressions and a Windows GitHub Actions smoke test.

Co-authored-by: Khaled Ghallab <171967089+Khaled-Ghallab@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-07-26 15:50:38 -05:00
co-authored by Khaled Ghallab
parent 4b2c775965
commit d7b5891b4f
7 changed files with 84 additions and 10 deletions
+38 -2
View File
@@ -88,11 +88,46 @@ jobs:
pnpm test packages/compiler/test/cc-driver.test.ts
--testNamePattern "host-native clang static build"
# Exercises the installed Windows toolchain and the built CLI end to end:
# TS7 must open its synthetic project, ambient files must resolve across
# slash styles, and the default executable must use the .exe suffix.
windows_cli:
name: test (windows CLI smoke)
runs-on: windows-latest
timeout-minutes: 15
env:
SCRIPTC_NO_CACHE: "1"
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Windows path regressions
run: pnpm exec vitest run packages/compiler/test/ts7/program.test.ts packages/cli/test/paths.test.ts
- name: Windows scriptc run smoke
shell: pwsh
run: |
clang --version
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend c
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$actual = ($output -join "`n").Trim()
if ($actual -ne "hello world") {
throw "unexpected scriptc output: $actual"
}
# Aggregate gate with the pre-matrix job's name, so anything keyed on the
# single "test" check (branch protection, badges) keeps resolving. Fails
# unless every matrix shard and the native Linux/glibc build succeeded.
# unless every matrix shard and both platform integration jobs succeeded.
test:
needs: [tests, linux_host_clang]
needs: [tests, linux_host_clang, windows_cli]
if: always()
runs-on: ubuntu-latest
steps:
@@ -100,3 +135,4 @@ jobs:
run: |
test "${{ needs.tests.result }}" = success
test "${{ needs.linux_host_clang.result }}" = success
test "${{ needs.windows_cli.result }}" = success
+2 -1
View File
@@ -5,6 +5,7 @@ import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { analyze, compile, compileC, compileLibrary, renderAll, renderCoverage, resolveProvenanceSources, setProvenanceSources } from "@scriptc/compiler";
import { defaultExecutableName } from "./paths.js";
const USAGE = `scriptc — TypeScript/JavaScript to native executables (experimental)
@@ -226,7 +227,7 @@ async function main(): Promise<number> {
const outDir = values.out ? dirname(resolve(values.out)) : join(dirname(input), ".scriptc");
const stem = basename(input).replace(/\.(ts|js|mjs|cjs|c|ll)$/, "");
const outPath = values.out ? resolve(values.out) : join(outDir, stem);
const outPath = values.out ? resolve(values.out) : join(outDir, defaultExecutableName(stem));
const build = async (): Promise<string> => {
if (values["from-c"]) {
+5
View File
@@ -0,0 +1,5 @@
/** Default executable filename for the host. Explicit --out paths stay
* exact; only scriptc's generated default needs the Windows PE suffix. */
export function defaultExecutableName(stem: string, platform: NodeJS.Platform = process.platform): string {
return platform === "win32" ? `${stem}.exe` : stem;
}
+8
View File
@@ -0,0 +1,8 @@
import { expect, test } from "vitest";
import { defaultExecutableName } from "../src/paths.js";
test("default executable names use the Windows PE suffix", () => {
expect(defaultExecutableName("main", "win32")).toBe("main.exe");
expect(defaultExecutableName("main", "linux")).toBe("main");
expect(defaultExecutableName("main", "darwin")).toBe("main");
});
+10 -3
View File
@@ -9,11 +9,18 @@ import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
/** tsgo uses slash-normalized file names on Windows (for SourceFile names
* and virtual-FS callbacks), while Node's path APIs use backslashes there.
* POSIX backslashes stay literal: they are valid filename characters. */
export function tsgoPath(path: string, platform: NodeJS.Platform = process.platform): string {
return platform === "win32" ? path.replaceAll("\\", "/") : path;
}
/** Path of the shipped ambient declarations — the always-shipped CORE
* (comptime/__island_eval, setTimeout). Part of EVERY program scriptc
* builds, the project-world preflight program included. */
export function ambientDtsPath(): string {
return require.resolve("@scriptc/compiler/scriptc.d.ts");
return tsgoPath(require.resolve("@scriptc/compiler/scriptc.d.ts"));
}
/** Path of the shipped divergence/precision OVERRIDES (JSON.parse():
@@ -22,7 +29,7 @@ export function ambientDtsPath(): string {
* so a project that typechecks under its own tsc never fails preflight over
* an override-manufactured error (checkPreflight). */
export function overridesDtsPath(): string {
return require.resolve("@scriptc/compiler/scriptc-overrides.d.ts");
return tsgoPath(require.resolve("@scriptc/compiler/scriptc-overrides.d.ts"));
}
/** Path of the shipped FALLBACK declarations (console, process, node:fs) —
@@ -30,7 +37,7 @@ export function overridesDtsPath(): string {
* With @types/node, the project's real Node types stand in and this file
* stands down (its declaration forms would collide). */
export function fallbackDtsPath(): string {
return require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts");
return tsgoPath(require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts"));
}
/** True for files belonging to the adopted Node type surface: the
@@ -27,6 +27,7 @@ import type {
import type { SourceFile } from "typescript/unstable/ast";
import { CheckerFacade } from "./checker.js";
import { enumKeyOf, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js";
import { tsgoPath } from "../shared.js";
/** The compiler options our createProgram accepts: TS7's CompilerOptions
* shape (numeric enums for target/module/moduleResolution — the enums module
@@ -110,19 +111,19 @@ export class Ts7Host {
// string => virtual (or shadowed) hit; null => shadowed out of
// existence; undefined => real-FS fallthrough.
readFile: (fileName) => {
const virtual = virtualFiles.get(fileName);
const virtual = virtualFiles.get(tsgoPath(fileName));
if (virtual !== undefined) return virtual;
if (shadow === null) return undefined;
if (shadow.hideFile(fileName)) return null;
return shadow.readFile(fileName);
},
fileExists: (fileName) => {
if (virtualFiles.has(fileName)) return true;
if (virtualFiles.has(tsgoPath(fileName))) return true;
if (shadow !== null && shadow.hideFile(fileName)) return false;
return undefined;
},
directoryExists: () => undefined,
realpath: (path) => (virtualFiles.has(path) ? path : undefined),
realpath: (path) => (virtualFiles.has(tsgoPath(path)) ? path : undefined),
getAccessibleEntries: () => undefined,
},
});
@@ -130,7 +131,7 @@ export class Ts7Host {
/** Registers an in-memory file served to tsgo by the virtual-FS hooks. */
addVirtualFile(path: string, content: string): void {
this.virtualFiles.set(path, content);
this.virtualFiles.set(tsgoPath(path), content);
}
/** tsgo's own tsconfig parser (extends chains resolved server-side) — the
@@ -0,0 +1,16 @@
import { describe, expect, test } from "vitest";
import { tsgoPath } from "../../src/frontend/shared.js";
describe("tsgo virtual filesystem paths", () => {
test("matches slash-normalized Windows callback paths", () => {
expect(tsgoPath("C:\\Users\\Alice\\project\\tsconfig.json", "win32"))
.toBe("C:/Users/Alice/project/tsconfig.json");
expect(tsgoPath("C:/Users/Alice/project/tsconfig.json", "win32"))
.toBe("C:/Users/Alice/project/tsconfig.json");
});
test("preserves backslashes that are literal POSIX filename characters", () => {
expect(tsgoPath("/tmp/project\\name/tsconfig.json", "linux"))
.toBe("/tmp/project\\name/tsconfig.json");
});
});