sproutboat 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -1
- package/README.md +7 -0
- package/SURFACE.md +6 -4
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/bin/sproutboat.cjs +17 -19
- package/package.json +10 -3
- package/scripts/build-cli.ts +113 -40
- package/src/bearssl.ts +203 -50
- package/src/build.ts +25 -4
- package/src/compile.ts +31 -11
- package/src/main.ts +40 -0
- package/src/sqlite.ts +133 -25
- package/src/surface.ts +19 -2
- package/src/toolchain.ts +314 -61
package/src/toolchain.ts
CHANGED
|
@@ -15,7 +15,9 @@ import { homedir } from "node:os";
|
|
|
15
15
|
import { dirname, resolve } from "node:path";
|
|
16
16
|
// @ts-expect-error Bun's file loader returns the embedded asset path in a compiled executable.
|
|
17
17
|
import embeddedUwsArchive from "../vendor/uwebsockets-360c276d-musl.tar.xz" with { type: "file" };
|
|
18
|
+
import { BEARSSL_VERSION } from "./bearssl";
|
|
18
19
|
import { cachedPorfforRoot, PORFFOR_CHANNEL, PORFFOR_COMMIT } from "./porffor-toolchain";
|
|
20
|
+
import { SQLITE_VERSION } from "./sqlite";
|
|
19
21
|
|
|
20
22
|
export const ZIG_VERSION = "0.16.0";
|
|
21
23
|
|
|
@@ -59,7 +61,7 @@ function platformKey(): ZigPlatform {
|
|
|
59
61
|
|
|
60
62
|
export class ZigToolchainError extends Error {
|
|
61
63
|
constructor(
|
|
62
|
-
readonly kind: "download" | "integrity" | "archive" | "cache" | "unsupported",
|
|
64
|
+
readonly kind: "download" | "integrity" | "archive" | "cache" | "compiler" | "unsupported",
|
|
63
65
|
message: string,
|
|
64
66
|
) {
|
|
65
67
|
super(message);
|
|
@@ -73,6 +75,8 @@ export type EnsureZigOptions = {
|
|
|
73
75
|
expectedSha256?: string;
|
|
74
76
|
fetcher?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
75
77
|
timeoutMs?: number;
|
|
78
|
+
/** Test hook: release acquisition always validates the cross-C++ toolchain. */
|
|
79
|
+
validate?: boolean;
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
async function sha256File(path: string): Promise<string> {
|
|
@@ -90,6 +94,7 @@ async function zigComplete(dir: string, key: ZigPlatform, expectedArchive: strin
|
|
|
90
94
|
platform?: string;
|
|
91
95
|
archiveSha256?: string;
|
|
92
96
|
binarySha256?: string;
|
|
97
|
+
cValidated?: boolean;
|
|
93
98
|
};
|
|
94
99
|
const bin = resolve(dir, "zig");
|
|
95
100
|
return (
|
|
@@ -97,6 +102,7 @@ async function zigComplete(dir: string, key: ZigPlatform, expectedArchive: strin
|
|
|
97
102
|
manifest.platform === key &&
|
|
98
103
|
manifest.archiveSha256 === expectedArchive &&
|
|
99
104
|
manifest.binarySha256 !== undefined &&
|
|
105
|
+
manifest.cValidated !== undefined &&
|
|
100
106
|
(await sha256File(bin)) === manifest.binarySha256
|
|
101
107
|
);
|
|
102
108
|
} catch {
|
|
@@ -144,6 +150,30 @@ async function zigDownloadUrls(
|
|
|
144
150
|
return [official];
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
/** Confirm the downloaded compiler can produce the C target Sproutboat builds. */
|
|
154
|
+
async function validateZigTarget(bin: string, root: string): Promise<void> {
|
|
155
|
+
const stage = resolve(root, `.zig-c-probe-${process.pid}-${crypto.randomUUID()}`);
|
|
156
|
+
try {
|
|
157
|
+
await mkdir(stage);
|
|
158
|
+
const source = resolve(stage, "main.c");
|
|
159
|
+
const out = resolve(stage, "probe");
|
|
160
|
+
await writeFile(source, "int main() { return 0; }\n");
|
|
161
|
+
const child = Bun.spawn([bin, "cc", "-target", "x86_64-linux-musl", "-static", source, "-o", out], {
|
|
162
|
+
stdout: "pipe",
|
|
163
|
+
stderr: "pipe",
|
|
164
|
+
});
|
|
165
|
+
const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
166
|
+
if (code !== 0 || !existsSync(out))
|
|
167
|
+
throw new ZigToolchainError(
|
|
168
|
+
"compiler",
|
|
169
|
+
`pinned Zig cannot link a linux-x86_64-musl C binary: ${stderr.trim() || `exit ${code}`}\n` +
|
|
170
|
+
"Set SPROUTBOAT_ZIG to a working Zig binary, then run `sproutboat toolchain doctor`.",
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await rm(stage, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
147
177
|
async function downloadZig(
|
|
148
178
|
urls: string[],
|
|
149
179
|
path: string,
|
|
@@ -218,19 +248,40 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
218
248
|
const stage = resolve(root, `.zig-${ZIG_VERSION}-${key}-${process.pid}-${crypto.randomUUID()}`);
|
|
219
249
|
const fetcher = options.fetcher ?? fetch;
|
|
220
250
|
const sourceUrl = options.url ?? process.env.SPROUTBOAT_ZIG_URL;
|
|
221
|
-
|
|
222
|
-
|
|
251
|
+
// A platform package ships its own host's Zig archive right beside the CLI
|
|
252
|
+
// executable (scripts/build-cli.ts stages it there at publish time, the same
|
|
253
|
+
// compressed .tar.xz an ordinary download would fetch), so a packaged
|
|
254
|
+
// install needs no network for Zig -- same pattern as the vendored
|
|
255
|
+
// uWebSockets archive. Only the default, no-`platform`/no-`url` call (every
|
|
256
|
+
// real caller) takes this path; build-cli.ts's own cross-target fetches and
|
|
257
|
+
// this file's tests always pass one of those and go straight to the network.
|
|
258
|
+
const vendored =
|
|
259
|
+
options.platform === undefined && !sourceUrl ? resolve(dirname(process.execPath), "zig.tar.xz") : null;
|
|
260
|
+
const usingVendored = vendored !== null && existsSync(vendored);
|
|
261
|
+
console.log(
|
|
262
|
+
usingVendored
|
|
263
|
+
? `Extracting vendored Zig ${ZIG_VERSION} (${key}, one-time)...`
|
|
264
|
+
: `Fetching Zig ${ZIG_VERSION} (${key}, one-time)...`,
|
|
265
|
+
);
|
|
223
266
|
try {
|
|
224
267
|
if (await zigComplete(dir, key, expected)) return resolve(dir, "zig");
|
|
225
268
|
await rm(dir, { recursive: true, force: true });
|
|
226
269
|
await mkdir(stage);
|
|
227
270
|
const archive = resolve(stage, "zig.tar.xz");
|
|
228
|
-
|
|
271
|
+
if (usingVendored) {
|
|
272
|
+
// SAFETY: usingVendored is only true when `vendored` is the non-null path just checked.
|
|
273
|
+
await Bun.write(archive, Bun.file(vendored!));
|
|
274
|
+
} else {
|
|
275
|
+
// Zig's pinned archive is about 50 MB. Keep the fetch bounded while
|
|
276
|
+
// allowing a cold download to complete on ordinary consumer connections.
|
|
277
|
+
const urls = sourceUrl ? [sourceUrl] : await zigDownloadUrls(key, fetcher, options.timeoutMs ?? 30_000);
|
|
278
|
+
await downloadZig(urls, archive, fetcher, options.timeoutMs ?? 120_000);
|
|
279
|
+
}
|
|
229
280
|
const actual = await sha256File(archive);
|
|
230
281
|
if (actual !== expected)
|
|
231
282
|
throw new ZigToolchainError(
|
|
232
283
|
"integrity",
|
|
233
|
-
|
|
284
|
+
`${usingVendored ? "vendored " : ""}Zig archive sha256 mismatch\n expected ${expected}\n got ${actual}`,
|
|
234
285
|
);
|
|
235
286
|
const untar = Bun.spawn(["tar", "-xJf", archive, "-C", stage, "--strip-components=1"], {
|
|
236
287
|
stdout: "pipe",
|
|
@@ -243,6 +294,7 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
243
294
|
const bin = resolve(stage, "zig");
|
|
244
295
|
if (!existsSync(bin)) throw new ZigToolchainError("archive", "Zig archive did not contain a `zig` binary");
|
|
245
296
|
await chmod(bin, 0o755);
|
|
297
|
+
if (options.validate !== false) await validateZigTarget(bin, root);
|
|
246
298
|
await writeFile(
|
|
247
299
|
resolve(stage, ".sproutboat-complete"),
|
|
248
300
|
JSON.stringify({
|
|
@@ -250,6 +302,7 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
250
302
|
platform: key,
|
|
251
303
|
archiveSha256: actual,
|
|
252
304
|
binarySha256: await sha256File(bin),
|
|
305
|
+
cValidated: options.validate !== false,
|
|
253
306
|
}),
|
|
254
307
|
{ mode: 0o444 },
|
|
255
308
|
);
|
|
@@ -261,6 +314,134 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
261
314
|
}
|
|
262
315
|
}
|
|
263
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The verified, compressed Zig archive for `platform` -- not extracted, not
|
|
319
|
+
* necessarily this host's own. `scripts/build-cli.ts` uses this to fetch and
|
|
320
|
+
* stage another platform's archive into that platform's package for `--all`,
|
|
321
|
+
* where `ensureZig` itself cannot help: it extracts, validates, and caches for
|
|
322
|
+
* *this* host only, none of which makes sense for a foreign target's binary.
|
|
323
|
+
* Cached at `<cacheRoot>/zig-archive-<version>-<platform>.tar.xz` so a repeat
|
|
324
|
+
* `--all` run does not re-download all four.
|
|
325
|
+
*/
|
|
326
|
+
export async function ensureZigArchive(platform: ZigPlatform, cacheRoot?: string): Promise<string> {
|
|
327
|
+
const root = resolve(cacheRoot ?? process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"));
|
|
328
|
+
const archive = resolve(root, `zig-archive-${ZIG_VERSION}-${platform}.tar.xz`);
|
|
329
|
+
const expected = ZIG_SHA256[platform];
|
|
330
|
+
if (existsSync(archive) && (await sha256File(archive)) === expected) return archive;
|
|
331
|
+
await mkdir(root, { recursive: true });
|
|
332
|
+
const stage = `${archive}.${process.pid}.${crypto.randomUUID()}`;
|
|
333
|
+
const urls = await zigDownloadUrls(platform, fetch, 30_000);
|
|
334
|
+
await downloadZig(urls, stage, fetch, 120_000);
|
|
335
|
+
const actual = await sha256File(stage);
|
|
336
|
+
if (actual !== expected) {
|
|
337
|
+
await rm(stage, { force: true });
|
|
338
|
+
throw new ZigToolchainError(
|
|
339
|
+
"integrity",
|
|
340
|
+
`Zig archive sha256 mismatch\n expected ${expected}\n got ${actual}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
await rename(stage, archive);
|
|
344
|
+
return archive;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** `cc [args...]`/`cxx [args...]`/`ar [args...]`, each as `[bin, ...leadingArgs]`
|
|
348
|
+
* ready to spread before a call's own arguments. */
|
|
349
|
+
export type HostCompiler = { cc: string[]; cxx: string[]; ar: string[]; usingZig: boolean };
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The C/C++/archiver toolchain for a **host** build (`dev`, `build --target
|
|
353
|
+
* host`, and every host-native object BearSSL/SQLite/uWebSockets compile).
|
|
354
|
+
*
|
|
355
|
+
* Prefers whatever the machine already has — no download, and unchanged
|
|
356
|
+
* behavior for every developer with Xcode Command Line Tools / build-essential
|
|
357
|
+
* installed. Falls back to the pinned Zig (already vendored for the musl
|
|
358
|
+
* cross-target) as a cross-compiler pointed at this same host, so the packaged
|
|
359
|
+
* CLI needs no system C toolchain at all — Zig ships a real native codegen
|
|
360
|
+
* backend for the host triple, not just cross-musl.
|
|
361
|
+
*/
|
|
362
|
+
export async function resolveHostCompiler(): Promise<HostCompiler> {
|
|
363
|
+
const cc = process.env.CC;
|
|
364
|
+
const cxx = process.env.CXX;
|
|
365
|
+
const ar = process.env.AR;
|
|
366
|
+
const haveCc = Boolean(cc || Bun.which("cc"));
|
|
367
|
+
const haveAr = Boolean(ar || Bun.which("ar"));
|
|
368
|
+
if (haveCc && haveAr) return { cc: [cc || "cc"], cxx: [cxx || "c++"], ar: [ar || "ar"], usingZig: false };
|
|
369
|
+
const zigBin = await ensureZig();
|
|
370
|
+
const [arch, os] = platformKey().split("-");
|
|
371
|
+
const triple = os === "macos" ? `${arch}-macos` : `${arch}-linux-gnu`;
|
|
372
|
+
return {
|
|
373
|
+
cc: [zigBin, "cc", "-target", triple],
|
|
374
|
+
cxx: [zigBin, "c++", "-target", triple],
|
|
375
|
+
ar: [zigBin, "ar"],
|
|
376
|
+
usingZig: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export type ToolchainDoctor = {
|
|
381
|
+
host: `${string}/${string}`;
|
|
382
|
+
cacheRoot: string;
|
|
383
|
+
porffor: { version: string; path: string; override: string | null; present: boolean };
|
|
384
|
+
zig: {
|
|
385
|
+
version: string;
|
|
386
|
+
platform: ZigPlatform | null;
|
|
387
|
+
path: string | null;
|
|
388
|
+
override: string | null;
|
|
389
|
+
present: boolean;
|
|
390
|
+
};
|
|
391
|
+
sqlite: { version: string; path: string; present: boolean };
|
|
392
|
+
bearssl: { version: string; path: string; present: boolean };
|
|
393
|
+
prerequisites: { cc: string | null; ar: string | null; tar: string | null; xcrun: string | null; sdk: string | null };
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/** Inspect the selected toolchain without downloading, compiling, or mutating a cache. */
|
|
397
|
+
export function inspectToolchain(): ToolchainDoctor {
|
|
398
|
+
const cacheRoot = resolve(process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"));
|
|
399
|
+
const porfforOverride = process.env.SPROUTBOAT_PORFFOR_DIR ? resolve(process.env.SPROUTBOAT_PORFFOR_DIR) : null;
|
|
400
|
+
const zigOverride = process.env.SPROUTBOAT_ZIG ? resolve(process.env.SPROUTBOAT_ZIG) : null;
|
|
401
|
+
let platform: ZigPlatform | null = null;
|
|
402
|
+
try {
|
|
403
|
+
platform = platformKey();
|
|
404
|
+
} catch {
|
|
405
|
+
/* doctor reports an unsupported host instead of throwing before its diagnostics */
|
|
406
|
+
}
|
|
407
|
+
const zigPath = zigOverride ?? (platform ? resolve(cacheRoot, `zig-${ZIG_VERSION}-${platform}`, "zig") : null);
|
|
408
|
+
const porfforPath = porfforOverride ?? cachedPorfforRoot(cacheRoot);
|
|
409
|
+
const sqlitePath = resolve(cacheRoot, `sqlite-${SQLITE_VERSION}`);
|
|
410
|
+
const bearsslPath = resolve(cacheRoot, `bearssl-${BEARSSL_VERSION}`);
|
|
411
|
+
const xcrun = process.platform === "darwin" ? Bun.which("xcrun") : null;
|
|
412
|
+
const sdk = (() => {
|
|
413
|
+
if (!xcrun) return null;
|
|
414
|
+
const probe = Bun.spawnSync([xcrun, "--show-sdk-path"], { stdout: "pipe", stderr: "ignore" });
|
|
415
|
+
return probe.exitCode === 0 ? probe.stdout.toString().trim() || null : null;
|
|
416
|
+
})();
|
|
417
|
+
return {
|
|
418
|
+
host: `${process.arch}/${process.platform}`,
|
|
419
|
+
cacheRoot,
|
|
420
|
+
porffor: {
|
|
421
|
+
version: porfforVersion(),
|
|
422
|
+
path: porfforPath,
|
|
423
|
+
override: porfforOverride,
|
|
424
|
+
present: existsSync(resolve(porfforPath, "runtime/index.js")),
|
|
425
|
+
},
|
|
426
|
+
zig: {
|
|
427
|
+
version: ZIG_VERSION,
|
|
428
|
+
platform,
|
|
429
|
+
path: zigPath,
|
|
430
|
+
override: zigOverride,
|
|
431
|
+
present: Boolean(zigPath && existsSync(zigPath)),
|
|
432
|
+
},
|
|
433
|
+
sqlite: { version: SQLITE_VERSION, path: sqlitePath, present: existsSync(sqlitePath) },
|
|
434
|
+
bearssl: { version: BEARSSL_VERSION, path: bearsslPath, present: existsSync(bearsslPath) },
|
|
435
|
+
prerequisites: {
|
|
436
|
+
cc: Bun.which(process.env.CC ?? "cc"),
|
|
437
|
+
ar: Bun.which(process.env.AR ?? "ar"),
|
|
438
|
+
tar: Bun.which("tar"),
|
|
439
|
+
xcrun,
|
|
440
|
+
sdk,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
264
445
|
function uwsCommitFull(): string {
|
|
265
446
|
try {
|
|
266
447
|
const src = readFileSync(resolve(porfforRoot(), "compiler/uwebsockets.js"), "utf8");
|
|
@@ -280,6 +461,65 @@ export function uwsVendorArchive(short: string): string {
|
|
|
280
461
|
: resolve(import.meta.dir, "..", "vendor", `uwebsockets-${short}-musl.tar.xz`);
|
|
281
462
|
}
|
|
282
463
|
|
|
464
|
+
const UWS_REQUIRED = ["src/App.h", "uSockets/uSockets.a"];
|
|
465
|
+
|
|
466
|
+
async function uwsComplete(dir: string, command?: string[]): Promise<boolean> {
|
|
467
|
+
try {
|
|
468
|
+
// SAFETY: this manifest is private data written below. The files it names
|
|
469
|
+
// are fixed by this module and each recorded digest is recomputed.
|
|
470
|
+
const manifest = JSON.parse(await readFile(resolve(dir, ".sproutboat-complete"), "utf8")) as {
|
|
471
|
+
files?: Record<string, string>;
|
|
472
|
+
command?: string[];
|
|
473
|
+
};
|
|
474
|
+
return (
|
|
475
|
+
(command === undefined || JSON.stringify(manifest.command) === JSON.stringify(command)) &&
|
|
476
|
+
(
|
|
477
|
+
await Promise.all(
|
|
478
|
+
UWS_REQUIRED.map(async (file) => (await sha256File(resolve(dir, file))) === manifest.files?.[file]),
|
|
479
|
+
)
|
|
480
|
+
).every(Boolean)
|
|
481
|
+
);
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function writeUwsManifest(dir: string, command?: string[]): Promise<void> {
|
|
488
|
+
const files = Object.fromEntries(
|
|
489
|
+
await Promise.all(UWS_REQUIRED.map(async (file) => [file, await sha256File(resolve(dir, file))] as const)),
|
|
490
|
+
);
|
|
491
|
+
const manifest = resolve(dir, ".sproutboat-complete");
|
|
492
|
+
const stage = `${manifest}.${process.pid}.${crypto.randomUUID()}`;
|
|
493
|
+
await writeFile(stage, JSON.stringify({ files, command }), { mode: 0o444 });
|
|
494
|
+
await rename(stage, manifest);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Serialize cache reconstruction and recover a lock left by an interrupted build. */
|
|
498
|
+
async function withUwsLock<T>(dir: string, action: () => Promise<T>): Promise<T> {
|
|
499
|
+
await mkdir(dirname(dir), { recursive: true });
|
|
500
|
+
const lock = `${dir}.lock`;
|
|
501
|
+
const deadline = Date.now() + 30_000;
|
|
502
|
+
for (;;) {
|
|
503
|
+
try {
|
|
504
|
+
await mkdir(lock);
|
|
505
|
+
break;
|
|
506
|
+
} catch {
|
|
507
|
+
if (Date.now() > deadline) throw new UwsUnavailableError(`timed out waiting for uWebSockets cache lock ${lock}`);
|
|
508
|
+
try {
|
|
509
|
+
if (Date.now() - (await stat(lock)).mtimeMs > 5 * 60_000) await rm(lock, { recursive: true, force: true });
|
|
510
|
+
} catch {
|
|
511
|
+
/* the active publisher released the lock between stat and rm */
|
|
512
|
+
}
|
|
513
|
+
await Bun.sleep(25);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
return await action();
|
|
518
|
+
} finally {
|
|
519
|
+
await rm(lock, { recursive: true, force: true });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
283
523
|
/**
|
|
284
524
|
* Seed `~/.cache/porffor/deps/uWebSockets-<commit>-musl/` with the checked-out,
|
|
285
525
|
* patched, `x86_64-linux-musl`-built uWebSockets tree so Porffor's own
|
|
@@ -297,30 +537,33 @@ export async function ensureUWebSockets(): Promise<void> {
|
|
|
297
537
|
const short = commit.slice(0, 8);
|
|
298
538
|
const depsRoot = resolve(homedir(), ".cache/porffor/deps");
|
|
299
539
|
const dir = resolve(depsRoot, `uWebSockets-${commit}-musl`);
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
? `SPROUTBOAT_UWS_TARBALL=${archive} does not exist`
|
|
307
|
-
: `no vendored uWebSockets archive at ${archive} (porffor pin moved? run \`bun tools/prebuild-uws.ts\`)`,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
if (!process.env.SPROUTBOAT_UWS_TARBALL) {
|
|
311
|
-
const actual = await sha256File(archive);
|
|
312
|
-
if (actual !== UWS_TARBALL_SHA256) {
|
|
540
|
+
if (await uwsComplete(dir)) return;
|
|
541
|
+
|
|
542
|
+
await withUwsLock(dir, async () => {
|
|
543
|
+
if (await uwsComplete(dir)) return;
|
|
544
|
+
const archive = process.env.SPROUTBOAT_UWS_TARBALL || uwsVendorArchive(short);
|
|
545
|
+
if (!existsSync(archive)) {
|
|
313
546
|
throw new UwsUnavailableError(
|
|
314
|
-
|
|
547
|
+
process.env.SPROUTBOAT_UWS_TARBALL
|
|
548
|
+
? `SPROUTBOAT_UWS_TARBALL=${archive} does not exist`
|
|
549
|
+
: `no vendored uWebSockets archive at ${archive} (porffor pin moved? run \`bun tools/prebuild-uws.ts\`)`,
|
|
315
550
|
);
|
|
316
551
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
552
|
+
if (!process.env.SPROUTBOAT_UWS_TARBALL) {
|
|
553
|
+
const actual = await sha256File(archive);
|
|
554
|
+
if (actual !== UWS_TARBALL_SHA256) {
|
|
555
|
+
throw new UwsUnavailableError(
|
|
556
|
+
`vendored uWebSockets sha256 mismatch\n expected ${UWS_TARBALL_SHA256}\n got ${actual}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
await extractUws(archive, dir);
|
|
561
|
+
});
|
|
320
562
|
}
|
|
321
563
|
|
|
322
564
|
/** Unpack the vendored source tree into `dir`. */
|
|
323
565
|
async function extractUws(archive: string, dir: string): Promise<void> {
|
|
566
|
+
await rm(dir, { recursive: true, force: true });
|
|
324
567
|
await mkdir(dir, { recursive: true });
|
|
325
568
|
// External programs cannot read Bun's virtual /$bunfs paths. Materialize the
|
|
326
569
|
// embedded release asset inside the destination before handing it to tar.
|
|
@@ -336,6 +579,12 @@ async function extractUws(archive: string, dir: string): Promise<void> {
|
|
|
336
579
|
await rm(dir, { recursive: true, force: true });
|
|
337
580
|
throw new UwsUnavailableError(`could not extract vendored uWebSockets: ${err.trim()}`);
|
|
338
581
|
}
|
|
582
|
+
try {
|
|
583
|
+
await writeUwsManifest(dir);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
await rm(dir, { recursive: true, force: true });
|
|
586
|
+
throw new UwsUnavailableError(`could not validate vendored uWebSockets: ${String(error)}`);
|
|
587
|
+
}
|
|
339
588
|
}
|
|
340
589
|
|
|
341
590
|
/**
|
|
@@ -358,50 +607,54 @@ export async function ensureUWebSocketsHost(): Promise<void> {
|
|
|
358
607
|
const dir = resolve(homedir(), ".cache/porffor/deps", `uWebSockets-${commit}`);
|
|
359
608
|
const uSockets = resolve(dir, "uSockets");
|
|
360
609
|
const archivePath = resolve(uSockets, "uSockets.a");
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (
|
|
610
|
+
const { cc, ar, usingZig } = await resolveHostCompiler();
|
|
611
|
+
const command = [cc.join(" "), ar.join(" ")];
|
|
612
|
+
if (await uwsComplete(dir, command)) return;
|
|
613
|
+
|
|
614
|
+
await withUwsLock(dir, async () => {
|
|
615
|
+
if (await uwsComplete(dir, command)) return;
|
|
616
|
+
const vendored = process.env.SPROUTBOAT_UWS_TARBALL || uwsVendorArchive(commit.slice(0, 8));
|
|
617
|
+
if (!existsSync(vendored)) throw new UwsUnavailableError(`no vendored uWebSockets archive at ${vendored}`);
|
|
618
|
+
if (!process.env.SPROUTBOAT_UWS_TARBALL) {
|
|
619
|
+
const actual = await sha256File(vendored);
|
|
620
|
+
if (actual !== UWS_TARBALL_SHA256) {
|
|
621
|
+
throw new UwsUnavailableError(
|
|
622
|
+
`vendored uWebSockets sha256 mismatch\n expected ${UWS_TARBALL_SHA256}\n got ${actual}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
await extractUws(vendored, dir);
|
|
627
|
+
|
|
628
|
+
// The archive carries the musl-built uSockets.a. Linking that into a host
|
|
629
|
+
// binary fails in a way nobody would connect to this, so it goes first.
|
|
630
|
+
await rm(archivePath, { force: true });
|
|
631
|
+
|
|
632
|
+
const sources = ["src/*.c", "src/eventing/*.c", "src/crypto/*.c", "src/io_uring/*.c"];
|
|
633
|
+
// Zig's cc needs `-fuse-ld=lld` wired up for LTO at link time; this is a
|
|
634
|
+
// `-c`-only compile step with no link, so LTO just isn't worth chasing here.
|
|
635
|
+
const flto = usingZig ? "" : "-flto ";
|
|
636
|
+
const compile = Bun.spawn(
|
|
637
|
+
["sh", "-c", `${command[0]} -std=c11 -Isrc -DLIBUS_NO_SSL ${flto}-O3 -c ${sources.join(" ")}`],
|
|
638
|
+
{ cwd: uSockets, stdout: "pipe", stderr: "pipe" },
|
|
639
|
+
);
|
|
640
|
+
const [ccCode, ccErr] = await Promise.all([compile.exited, new Response(compile.stderr).text()]);
|
|
641
|
+
if (ccCode !== 0) {
|
|
370
642
|
throw new UwsUnavailableError(
|
|
371
|
-
`
|
|
643
|
+
`could not compile uSockets with ${command[0]}: ${ccErr.trim().split("\n").slice(-3).join(" ")}`,
|
|
372
644
|
);
|
|
373
645
|
}
|
|
374
|
-
}
|
|
375
|
-
if (!existsSync(resolve(dir, "src/App.h"))) await extractUws(vendored, dir);
|
|
376
646
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
stderr: "pipe",
|
|
388
|
-
});
|
|
389
|
-
const [ccCode, ccErr] = await Promise.all([compile.exited, new Response(compile.stderr).text()]);
|
|
390
|
-
if (ccCode !== 0) {
|
|
391
|
-
throw new UwsUnavailableError(
|
|
392
|
-
`could not compile uSockets with ${cc}: ${ccErr.trim().split("\n").slice(-3).join(" ")}`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const archiveStep = Bun.spawn(["sh", "-c", `${ar} rvs uSockets.a *.o`], {
|
|
397
|
-
cwd: uSockets,
|
|
398
|
-
stdout: "pipe",
|
|
399
|
-
stderr: "pipe",
|
|
647
|
+
const archiveStep = Bun.spawn(["sh", "-c", `${command[1]} rvs uSockets.a *.o`], {
|
|
648
|
+
cwd: uSockets,
|
|
649
|
+
stdout: "pipe",
|
|
650
|
+
stderr: "pipe",
|
|
651
|
+
});
|
|
652
|
+
const [arCode, arErr] = await Promise.all([archiveStep.exited, new Response(archiveStep.stderr).text()]);
|
|
653
|
+
if (arCode !== 0 || !existsSync(archivePath)) {
|
|
654
|
+
throw new UwsUnavailableError(`could not archive uSockets with ${command[1]}: ${arErr.trim()}`);
|
|
655
|
+
}
|
|
656
|
+
await writeUwsManifest(dir, command);
|
|
400
657
|
});
|
|
401
|
-
const [arCode, arErr] = await Promise.all([archiveStep.exited, new Response(archiveStep.stderr).text()]);
|
|
402
|
-
if (arCode !== 0 || !existsSync(archivePath)) {
|
|
403
|
-
throw new UwsUnavailableError(`could not archive uSockets with ${ar}: ${arErr.trim()}`);
|
|
404
|
-
}
|
|
405
658
|
}
|
|
406
659
|
|
|
407
660
|
/** Directory holding node_modules/porffor (walks up from this file). */
|