-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmod.js
More file actions
253 lines (230 loc) · 9.39 KB
/
Copy pathmod.js
File metadata and controls
253 lines (230 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// SPDX-License-Identifier: PMPL-1.0-or-later
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// @hyperpolymath/affinescript — the thin compiler shim (ADR-019 / #260 S3).
//
// The AffineScript compiler is a native OCaml binary, not a JS package.
// Per ADR-019 the GitHub Release (cut by .github/workflows/release.yml,
// #260 S2) is the CANONICAL artifact: per-platform `affinescript-<target>`
// binaries + a `SHA256SUMS` manifest. This package is the ergonomic
// front door: it downloads the binary for the host triple from the
// Release pinned by THIS package version, verifies it against the
// checksum embedded here (no floating fetch — one version+checksum per
// shim release, the ADR-019 supply-chain rule), caches it, and execs it
// with the caller's argv.
//
// Runtime support — Deno is canonical; Bun and Node.js are first-class
// targets because the shim's consumers (LSP installers, IDE extensions,
// build scripts) often run in those environments. All three branches
// are guarded by runtime detection at module load; nothing dynamically
// imports a foreign runtime's globals. Browsers and Cloudflare Workers
// are NOT supported and never will be: the shim's purpose is "fetch,
// save to disk, exec a native binary" — steps 2 and 3 are not possible
// in a sandboxed JS runtime.
//
// JavaScript, not ReScript, because this is entirely host-API surface
// (spawn / fetch / crypto.subtle / fs) — the documented "JS only where
// ReScript cannot" carve-out, same as packages/affine-js.
/// <reference types="./mod.d.ts" />
import { PINS } from "./pins.js";
// ─── runtime detection ──────────────────────────────────────────────
const isDeno = typeof Deno !== "undefined";
const isBun = !isDeno && typeof Bun !== "undefined";
const isNode = !isDeno && !isBun &&
typeof process !== "undefined" && !!process.versions?.node;
if (!isDeno && !isBun && !isNode) {
throw new Error(
"@hyperpolymath/affinescript: unsupported runtime. " +
"This package targets Deno (canonical), Bun, and Node.js. " +
"Browsers and Workers cannot exec native binaries.",
);
}
// ─── host helpers ───────────────────────────────────────────────────
// Each helper picks one branch at runtime; the others are dead code in
// that process and never load. `await import("node:…")` in the Node
// branch works under Deno too (Deno node-compat), so a Bun path that
// shells out to node: APIs is still valid — but we keep Bun's native
// APIs where they exist for fewer surprises and better diagnostics.
function hostOs() {
// Deno: "linux"|"darwin"|"windows"|...
if (isDeno) return Deno.build.os;
// Bun and Node both populate process.platform identically.
return process.platform;
}
function hostArch() {
// Deno reports "x86_64"|"aarch64"; Node/Bun report "x64"|"arm64".
// Normalise to Deno's spelling — that's what hostTarget() matches.
if (isDeno) return Deno.build.arch;
if (process.arch === "x64") return "x86_64";
if (process.arch === "arm64") return "aarch64";
return process.arch;
}
function envGet(name) {
if (isDeno) return Deno.env.get(name);
return process.env[name];
}
async function readBytes(path) {
if (isDeno) return await Deno.readFile(path);
if (isBun) return new Uint8Array(await Bun.file(path).arrayBuffer());
const { readFile } = await import("node:fs/promises");
return new Uint8Array(await readFile(path));
}
async function writeBytes(path, bytes, { mode } = {}) {
if (isDeno) {
return await Deno.writeFile(path, bytes, mode != null ? { mode } : {});
}
if (isBun) {
await Bun.write(path, bytes);
if (mode != null) {
const { chmod } = await import("node:fs/promises");
await chmod(path, mode).catch(() => {});
}
return;
}
const { writeFile, chmod } = await import("node:fs/promises");
await writeFile(path, bytes);
if (mode != null) await chmod(path, mode).catch(() => {});
}
async function mkdirRecursive(path) {
if (isDeno) return await Deno.mkdir(path, { recursive: true });
const { mkdir } = await import("node:fs/promises");
await mkdir(path, { recursive: true });
}
async function chmodExec(path) {
if (isDeno) return await Deno.chmod(path, 0o755).catch(() => {});
const { chmod } = await import("node:fs/promises");
await chmod(path, 0o755).catch(() => {});
}
async function spawnInherit(bin, args) {
if (isDeno) {
const { code } = await new Deno.Command(bin, {
args,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
}).output();
return code;
}
if (isBun) {
// Bun.spawn returns a process whose `.exited` resolves to the code.
const proc = Bun.spawn([bin, ...args], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
return await proc.exited;
}
const { spawn } = await import("node:child_process");
return await new Promise((resolve, reject) => {
const child = spawn(bin, args, { stdio: "inherit" });
child.on("close", (code) => resolve(code ?? 0));
child.on("error", reject);
});
}
/** Does this process look like it was invoked as `<runtime> <thisfile>`? */
function thisIsMain() {
// Deno: import.meta.main; Bun also honours it. Node: compare URLs.
if (isDeno || isBun) return import.meta.main === true;
if (isNode) {
const arg1 = process.argv[1];
if (!arg1) return false;
try {
const here = new URL(import.meta.url).pathname;
return here === arg1 || here.endsWith(arg1);
} catch {
return false;
}
}
return false;
}
// ─── public API ─────────────────────────────────────────────────────
/** Map the host to an ADR-019 release target triple. */
export function hostTarget(os = hostOs(), arch = hostArch()) {
if (os === "linux" && arch === "x86_64") return "linux-x64";
if (os === "darwin" && arch === "x86_64") return "macos-x64";
if (os === "darwin" && arch === "aarch64") return "macos-arm64";
throw new Error(
`@hyperpolymath/affinescript: unsupported host ${os}/${arch}. ` +
`Supported: linux-x64, macos-x64, macos-arm64 (windows-x64 is a ` +
`tracked follow-up). Build from source: hyperpolymath/affinescript.`,
);
}
/** Lower-case hex of the SHA-256 of `bytes`. */
export async function sha256Hex(bytes) {
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
/** Cache path for a pinned binary (XDG, then HOME, then a temp dir). */
export function cachePath(version, target) {
const base = envGet("AFFINESCRIPT_CACHE") ??
envGet("XDG_CACHE_HOME") ??
(envGet("HOME") ? `${envGet("HOME")}/.cache` : null) ??
envGet("TMPDIR") ?? "/tmp";
return `${base}/affinescript/${version}/affinescript-${target}`;
}
/**
* Resolve a runnable compiler binary path for the host, downloading +
* checksum-verifying from the pinned Release on a cache miss.
*
* @param {{ pins?: object, fetchImpl?: typeof fetch }} [opts]
* `pins`/`fetchImpl` are test seams; production uses the embedded
* PINS and global fetch.
* @returns {Promise<string>} absolute path to the verified binary
*/
export async function resolveCompiler(opts = {}) {
const pins = opts.pins ?? PINS;
const doFetch = opts.fetchImpl ?? fetch;
const target = hostTarget();
const entry = pins.targets?.[target];
if (!entry || !entry.sha256 || !entry.url) {
throw new Error(
`@hyperpolymath/affinescript: no pinned binary for ${target} at ` +
`version ${pins.version} (pins.js not finalised for this release).`,
);
}
const path = cachePath(pins.version, target);
// Cache hit only counts if the cached bytes still match the pin
// (defends against a corrupted/tampered cache).
try {
const cached = await readBytes(path);
if ((await sha256Hex(cached)) === entry.sha256) return path;
} catch { /* miss — fall through to download */ }
const res = await doFetch(entry.url);
if (!res.ok) {
throw new Error(
`@hyperpolymath/affinescript: download failed for ${target} ` +
`(${entry.url}): HTTP ${res.status}`,
);
}
const bytes = new Uint8Array(await res.arrayBuffer());
const got = await sha256Hex(bytes);
if (got !== entry.sha256) {
throw new Error(
`@hyperpolymath/affinescript: checksum mismatch for ${target} — ` +
`expected ${entry.sha256}, got ${got}. Refusing to run (the ` +
`Release artifact does not match this shim version's pin).`,
);
}
await mkdirRecursive(path.slice(0, path.lastIndexOf("/")));
await writeBytes(path, bytes, { mode: 0o755 });
// writeBytes' mode is pre-umask on some FSes; ensure the executable
// bit is set so the spawn doesn't fail with EACCES.
await chmodExec(path);
return path;
}
/**
* Resolve then exec the compiler with `args`, inheriting stdio.
* Returns the child's exit code (caller decides whether to exit).
*/
export async function run(args = [], opts = {}) {
const bin = await resolveCompiler(opts);
return await spawnInherit(bin, args);
}
if (thisIsMain()) {
const argv = isDeno ? Deno.args : process.argv.slice(2);
const code = await run(argv);
if (isDeno) Deno.exit(code);
else process.exit(code);
}