sproutboat 0.10.2 → 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 +76 -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 +11 -4
- 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/types/sproutboat.d.ts +5 -1
package/src/bearssl.ts
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
19
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { resolve } from "node:path";
|
|
22
|
+
import { resolveHostCompiler } from "./toolchain";
|
|
22
23
|
|
|
23
24
|
export const BEARSSL_VERSION = "0.6";
|
|
24
25
|
const BEARSSL_URL = `https://bearssl.org/bearssl-${BEARSSL_VERSION}.tar.gz`;
|
|
@@ -36,7 +37,41 @@ const BEARSSL_SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139e
|
|
|
36
37
|
const CACERT_URL = "https://curl.se/ca/cacert.pem";
|
|
37
38
|
const CACERT_SHA256 = "f66dff1bdf8f96060b8177976f8b7d9254bc89bc4db933d769f7384d28480bc9";
|
|
38
39
|
|
|
39
|
-
const cacheDir = (): string =>
|
|
40
|
+
const cacheDir = (): string =>
|
|
41
|
+
resolve(
|
|
42
|
+
process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"),
|
|
43
|
+
`bearssl-${BEARSSL_VERSION}`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const digest = async (path: string): Promise<string> =>
|
|
47
|
+
createHash("sha256")
|
|
48
|
+
.update(await readFile(path))
|
|
49
|
+
.digest("hex");
|
|
50
|
+
|
|
51
|
+
async function withBearsslLock<T>(dir: string, name: string, action: () => Promise<T>): Promise<T> {
|
|
52
|
+
await mkdir(dir, { recursive: true });
|
|
53
|
+
const lock = resolve(dir, `.${name}.lock`);
|
|
54
|
+
const deadline = Date.now() + 30_000;
|
|
55
|
+
for (;;) {
|
|
56
|
+
try {
|
|
57
|
+
await mkdir(lock);
|
|
58
|
+
break;
|
|
59
|
+
} catch {
|
|
60
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for BearSSL cache lock ${lock}`);
|
|
61
|
+
try {
|
|
62
|
+
if (Date.now() - (await stat(lock)).mtimeMs > 5 * 60_000) await rm(lock, { recursive: true, force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
/* publisher released the lock while it was being inspected */
|
|
65
|
+
}
|
|
66
|
+
await Bun.sleep(25);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return await action();
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(lock, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
40
75
|
|
|
41
76
|
export type BearsslInput = {
|
|
42
77
|
target: "linux-x86_64" | "host";
|
|
@@ -51,9 +86,18 @@ export type BearsslBuild = {
|
|
|
51
86
|
objects: string[];
|
|
52
87
|
};
|
|
53
88
|
|
|
54
|
-
const run = (
|
|
89
|
+
const run = (
|
|
90
|
+
cmd: string,
|
|
91
|
+
args: string[],
|
|
92
|
+
cwd?: string,
|
|
93
|
+
env?: Record<string, string | undefined>,
|
|
94
|
+
): Promise<{ code: number; stderr: string }> =>
|
|
55
95
|
new Promise((done) => {
|
|
56
|
-
const child = spawn(cmd, args, {
|
|
96
|
+
const child = spawn(cmd, args, {
|
|
97
|
+
cwd,
|
|
98
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
99
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
100
|
+
});
|
|
57
101
|
let stderr = "";
|
|
58
102
|
child.stderr.on("data", (chunk) => (stderr += String(chunk)));
|
|
59
103
|
child.on("close", (code) => done({ code: code ?? 1, stderr }));
|
|
@@ -73,13 +117,43 @@ async function download(url: string, expected: string, to: string): Promise<Buff
|
|
|
73
117
|
async function sourceTree(): Promise<string> {
|
|
74
118
|
const dir = cacheDir();
|
|
75
119
|
const tree = resolve(dir, `bearssl-${BEARSSL_VERSION}`);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
return withBearsslLock(dir, "source", async () => {
|
|
121
|
+
const tarball = resolve(dir, "bearssl.tar.gz");
|
|
122
|
+
const sourceManifest = resolve(tree, ".sproutboat-source");
|
|
123
|
+
try {
|
|
124
|
+
// SAFETY: this untrusted JSON is checked against pinned and computed hashes below.
|
|
125
|
+
const manifest = JSON.parse(await readFile(sourceManifest, "utf8")) as {
|
|
126
|
+
archive?: string;
|
|
127
|
+
header?: string;
|
|
128
|
+
};
|
|
129
|
+
if (manifest.archive === BEARSSL_SHA256 && manifest.header === (await digest(resolve(tree, "inc/bearssl.h"))))
|
|
130
|
+
return tree;
|
|
131
|
+
} catch {
|
|
132
|
+
/* rebuild an incomplete or corrupt source cache below */
|
|
133
|
+
}
|
|
134
|
+
await rm(tree, { recursive: true, force: true });
|
|
135
|
+
if (!existsSync(tarball) || (await digest(tarball)) !== BEARSSL_SHA256) {
|
|
136
|
+
await rm(tarball, { force: true });
|
|
137
|
+
await download(BEARSSL_URL, BEARSSL_SHA256, tarball);
|
|
138
|
+
}
|
|
139
|
+
const stage = resolve(dir, `.bearssl-stage-${process.pid}-${crypto.randomUUID()}`);
|
|
140
|
+
await mkdir(stage, { recursive: true });
|
|
141
|
+
const untar = await run("tar", ["xzf", tarball, "-C", stage]);
|
|
142
|
+
if (untar.code !== 0) throw new Error(`could not unpack BearSSL: ${untar.stderr}`);
|
|
143
|
+
const stagedTree = resolve(stage, `bearssl-${BEARSSL_VERSION}`);
|
|
144
|
+
if (!existsSync(resolve(stagedTree, "inc/bearssl.h"))) {
|
|
145
|
+
await rm(stage, { recursive: true, force: true });
|
|
146
|
+
throw new Error("BearSSL archive did not contain bearssl.h");
|
|
147
|
+
}
|
|
148
|
+
await writeFile(
|
|
149
|
+
resolve(stagedTree, ".sproutboat-source"),
|
|
150
|
+
JSON.stringify({ archive: BEARSSL_SHA256, header: await digest(resolve(stagedTree, "inc/bearssl.h")) }),
|
|
151
|
+
{ mode: 0o444 },
|
|
152
|
+
);
|
|
153
|
+
await rename(stagedTree, tree);
|
|
154
|
+
await rm(stage, { recursive: true, force: true });
|
|
155
|
+
return tree;
|
|
156
|
+
});
|
|
83
157
|
}
|
|
84
158
|
|
|
85
159
|
/** Every .c under src/, which is what the library is. */
|
|
@@ -103,7 +177,8 @@ async function sources(tree: string): Promise<string[]> {
|
|
|
103
177
|
async function trustAnchorTool(tree: string): Promise<string> {
|
|
104
178
|
const tool = resolve(tree, "build/brssl");
|
|
105
179
|
if (existsSync(tool)) return tool;
|
|
106
|
-
const
|
|
180
|
+
const { cc } = await resolveHostCompiler();
|
|
181
|
+
const make = await run("make", ["-j8", "build/brssl"], tree, { CC: cc.join(" ") });
|
|
107
182
|
if (!existsSync(tool)) throw new Error(`could not build brssl: ${make.stderr}`);
|
|
108
183
|
return tool;
|
|
109
184
|
}
|
|
@@ -117,27 +192,44 @@ async function trustAnchorTool(tree: string): Promise<string> {
|
|
|
117
192
|
*/
|
|
118
193
|
async function trustAnchorSource(tree: string): Promise<string> {
|
|
119
194
|
const out = resolve(cacheDir(), "trust-anchors.c");
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
195
|
+
return withBearsslLock(cacheDir(), "anchors", async () => {
|
|
196
|
+
const manifestPath = resolve(cacheDir(), ".sproutboat-anchors");
|
|
197
|
+
const pem = resolve(cacheDir(), "cacert.pem");
|
|
198
|
+
try {
|
|
199
|
+
// SAFETY: this untrusted JSON is checked against pinned and computed hashes below.
|
|
200
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as { pem?: string; source?: string };
|
|
201
|
+
if (manifest.pem === CACERT_SHA256 && manifest.source === (await digest(out))) return out;
|
|
202
|
+
} catch {
|
|
203
|
+
/* regenerate an incomplete or corrupt trust-anchor cache below */
|
|
204
|
+
}
|
|
205
|
+
if (!existsSync(pem) || (await digest(pem)) !== CACERT_SHA256) {
|
|
206
|
+
await rm(pem, { force: true });
|
|
207
|
+
await download(CACERT_URL, CACERT_SHA256, pem);
|
|
208
|
+
}
|
|
209
|
+
const tool = await trustAnchorTool(tree);
|
|
210
|
+
|
|
211
|
+
const generated = await new Promise<string>((done, fail) => {
|
|
212
|
+
const child = spawn(tool, ["ta", pem], { stdio: ["ignore", "pipe", "pipe"] });
|
|
213
|
+
let text = "";
|
|
214
|
+
let err = "";
|
|
215
|
+
child.stdout.on("data", (chunk) => (text += String(chunk)));
|
|
216
|
+
child.stderr.on("data", (chunk) => (err += String(chunk)));
|
|
217
|
+
child.on("close", (code) => (code === 0 ? done(text) : fail(new Error(`brssl ta failed: ${err}`))));
|
|
218
|
+
});
|
|
133
219
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
220
|
+
const count = /#define TAs_NUM\s+(\d+)/.exec(generated)?.[1];
|
|
221
|
+
if (!count) throw new Error("brssl ta produced no TAs_NUM — its output format changed");
|
|
222
|
+
const linkable = generated
|
|
223
|
+
.replace(/static const br_x509_trust_anchor TAs\[/, "const br_x509_trust_anchor sb_trust_anchors[")
|
|
224
|
+
.replace(/#define TAs_NUM\s+\d+/, `const size_t sb_trust_anchor_count = ${count};`);
|
|
225
|
+
const stage = `${out}.${process.pid}.${crypto.randomUUID()}`;
|
|
226
|
+
await writeFile(stage, `#include <stddef.h>\n#include "bearssl.h"\n${linkable}\n`);
|
|
227
|
+
await rename(stage, out);
|
|
228
|
+
const manifestStage = `${manifestPath}.${process.pid}.${crypto.randomUUID()}`;
|
|
229
|
+
await writeFile(manifestStage, JSON.stringify({ pem: CACERT_SHA256, source: await digest(out) }), { mode: 0o444 });
|
|
230
|
+
await rename(manifestStage, manifestPath);
|
|
231
|
+
return out;
|
|
232
|
+
});
|
|
141
233
|
}
|
|
142
234
|
|
|
143
235
|
/** Compile one C file, caching on the output path. */
|
|
@@ -152,29 +244,90 @@ export async function ensureBearssl(input: BearsslInput): Promise<BearsslBuild>
|
|
|
152
244
|
const tree = await sourceTree();
|
|
153
245
|
const includeDir = resolve(tree, "inc");
|
|
154
246
|
const objDir = resolve(cacheDir(), `obj-${input.target}`);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const [cc, prefix] =
|
|
247
|
+
const [cc, ...prefix] =
|
|
158
248
|
input.target === "host"
|
|
159
|
-
? (
|
|
160
|
-
:
|
|
249
|
+
? (await resolveHostCompiler()).cc
|
|
250
|
+
: [input.zigBin ?? "zig", "cc", "-target", "x86_64-linux-musl"];
|
|
161
251
|
const includes = [includeDir, resolve(tree, "src")];
|
|
252
|
+
const sourceFiles = await sources(tree);
|
|
253
|
+
const anchors = await trustAnchorSource(tree);
|
|
254
|
+
const sourceHashes = Object.fromEntries(
|
|
255
|
+
await Promise.all(sourceFiles.map(async (source) => [source, await digest(source)] as const)),
|
|
256
|
+
);
|
|
257
|
+
const anchorSha256 = await digest(anchors);
|
|
258
|
+
const manifestPath = resolve(objDir, ".sproutboat-complete");
|
|
259
|
+
const objectNames = [
|
|
260
|
+
...sourceFiles.map((source) => `${source.split("/").pop()!.replace(/\.c$/, "")}.o`),
|
|
261
|
+
"trust-anchors.o",
|
|
262
|
+
];
|
|
263
|
+
const flags = ["-Os", ...includes.flatMap((include) => ["-I", include])];
|
|
264
|
+
const validCache = async (): Promise<boolean> => {
|
|
265
|
+
try {
|
|
266
|
+
// SAFETY: the manifest is private data written below. Every source, target,
|
|
267
|
+
// compiler invocation, and object digest must match before reuse.
|
|
268
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as {
|
|
269
|
+
target?: string;
|
|
270
|
+
command?: string[];
|
|
271
|
+
flags?: string[];
|
|
272
|
+
sources?: Record<string, string>;
|
|
273
|
+
anchors?: string;
|
|
274
|
+
objects?: Record<string, string>;
|
|
275
|
+
};
|
|
276
|
+
const validObjects = await Promise.all(
|
|
277
|
+
objectNames.map(async (name) => manifest.objects?.[name] === (await digest(resolve(objDir, name)))),
|
|
278
|
+
);
|
|
279
|
+
return (
|
|
280
|
+
manifest.target === input.target &&
|
|
281
|
+
JSON.stringify(manifest.command) === JSON.stringify([cc, ...prefix]) &&
|
|
282
|
+
JSON.stringify(manifest.flags) === JSON.stringify(flags) &&
|
|
283
|
+
JSON.stringify(manifest.sources) === JSON.stringify(sourceHashes) &&
|
|
284
|
+
manifest.anchors === anchorSha256 &&
|
|
285
|
+
validObjects.every(Boolean)
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
if (await validCache()) return { includeDir, objects: objectNames.map((name) => resolve(objDir, name)) };
|
|
162
292
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const out = resolve(objDir, `${source.split("/").pop()!.replace(/\.c$/, "")}.o`);
|
|
168
|
-
await compile(cc, [...prefix], source, out, includes);
|
|
169
|
-
objects.push(out);
|
|
170
|
-
}
|
|
293
|
+
return withBearsslLock(cacheDir(), `objects-${input.target}`, async () => {
|
|
294
|
+
if (await validCache()) return { includeDir, objects: objectNames.map((name) => resolve(objDir, name)) };
|
|
295
|
+
await rm(objDir, { recursive: true, force: true });
|
|
296
|
+
await mkdir(objDir, { recursive: true });
|
|
171
297
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
298
|
+
const objects: string[] = [];
|
|
299
|
+
// Sequential rather than parallel: this runs once per target and a burst of
|
|
300
|
+
// ~180 compiler processes is a worse neighbour than a slow first build.
|
|
301
|
+
for (const source of sourceFiles) {
|
|
302
|
+
const out = resolve(objDir, `${source.split("/").pop()!.replace(/\.c$/, "")}.o`);
|
|
303
|
+
await compile(cc, [...prefix], source, out, includes);
|
|
304
|
+
objects.push(out);
|
|
305
|
+
}
|
|
176
306
|
|
|
177
|
-
|
|
307
|
+
const anchorObject = resolve(objDir, "trust-anchors.o");
|
|
308
|
+
await compile(cc, [...prefix], anchors, anchorObject, includes);
|
|
309
|
+
objects.push(anchorObject);
|
|
310
|
+
|
|
311
|
+
const objectHashes = Object.fromEntries(
|
|
312
|
+
await Promise.all(objects.map(async (object) => [object.split("/").pop()!, await digest(object)] as const)),
|
|
313
|
+
);
|
|
314
|
+
const stagedManifest = `${manifestPath}.${process.pid}.${crypto.randomUUID()}`;
|
|
315
|
+
await writeFile(
|
|
316
|
+
stagedManifest,
|
|
317
|
+
JSON.stringify({
|
|
318
|
+
target: input.target,
|
|
319
|
+
command: [cc, ...prefix],
|
|
320
|
+
flags,
|
|
321
|
+
sources: sourceHashes,
|
|
322
|
+
anchors: anchorSha256,
|
|
323
|
+
objects: objectHashes,
|
|
324
|
+
}),
|
|
325
|
+
{ mode: 0o444 },
|
|
326
|
+
);
|
|
327
|
+
await rename(stagedManifest, manifestPath);
|
|
328
|
+
|
|
329
|
+
return { includeDir, objects };
|
|
330
|
+
});
|
|
178
331
|
}
|
|
179
332
|
|
|
180
333
|
export const bearsslStamp = (): string => `bearssl/${BEARSSL_VERSION}`;
|
package/src/build.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { walkAssets, type AssetManifest } from "./assets";
|
|
5
5
|
import { resourceRefs, type SproutboatConfig } from "./config";
|
|
6
6
|
import { ensureSqliteObject } from "./sqlite";
|
|
7
|
-
import {
|
|
7
|
+
import { sqliteStamp } from "./sqlite";
|
|
8
|
+
import { bearsslStamp, ensureBearssl } from "./bearssl";
|
|
8
9
|
import { compileSprout, type Transport } from "./compile";
|
|
9
10
|
import {
|
|
10
11
|
ARTIFACT_SCHEMA_VERSION,
|
|
@@ -70,7 +71,29 @@ function digest(value: Uint8Array | string): `sha256:${string}` {
|
|
|
70
71
|
export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
71
72
|
const source = input.source ?? (await readFile(input.sourcePath));
|
|
72
73
|
const sourceHash = digest(source);
|
|
73
|
-
const
|
|
74
|
+
const target = input.target ?? "linux-x86_64";
|
|
75
|
+
const embedded = input.transport === "embedded";
|
|
76
|
+
// The directory identity must change whenever an input that changes the
|
|
77
|
+
// generated C or link line changes. A source digest alone reused an artifact
|
|
78
|
+
// path across compiler pins, targets, and standalone ABI settings.
|
|
79
|
+
const artifactId = digest(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
sourceHash,
|
|
82
|
+
target,
|
|
83
|
+
transport: input.transport ?? "broker",
|
|
84
|
+
optimize: input.optimize ?? "release",
|
|
85
|
+
compatibilityDate: input.config.compatibility_date,
|
|
86
|
+
config: input.config,
|
|
87
|
+
toolchain: toolchainStamp(),
|
|
88
|
+
compiler:
|
|
89
|
+
target === "host"
|
|
90
|
+
? `host:${process.env.CC ?? "cc"}`
|
|
91
|
+
: process.env.SPROUTBOAT_ZIG
|
|
92
|
+
? `override:${process.env.SPROUTBOAT_ZIG}`
|
|
93
|
+
: "managed-zig",
|
|
94
|
+
native: embedded ? [sqliteStamp(), bearsslStamp()] : [],
|
|
95
|
+
}),
|
|
96
|
+
).slice("sha256:".length, 24);
|
|
74
97
|
const artifactDir = input.outputDirectory ?? resolve(input.projectDir, ".sproutboat/dist", artifactId);
|
|
75
98
|
const sproutPath = resolve(artifactDir, "sprout");
|
|
76
99
|
await mkdir(artifactDir, { recursive: true });
|
|
@@ -116,8 +139,6 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
116
139
|
// #15 — an embedded sprout carries its own storage and TLS instead of talking
|
|
117
140
|
// to a broker: SQLite and BearSSL are compiled once per target and added to
|
|
118
141
|
// the link line, and BearSSL's header to the compile line.
|
|
119
|
-
const embedded = input.transport === "embedded";
|
|
120
|
-
const target = input.target ?? "linux-x86_64";
|
|
121
142
|
const sqliteObject = embedded ? await ensureSqliteObject({ target, zigBin }) : null;
|
|
122
143
|
const tls = embedded ? await ensureBearssl({ target, zigBin }) : null;
|
|
123
144
|
const extraLink = [...(sqliteObject ? [sqliteObject] : []), ...(tls ? tls.objects : [])];
|
package/src/compile.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { existsSync } from "node:fs";
|
|
|
14
14
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
15
15
|
import { dirname, resolve } from "node:path";
|
|
16
16
|
import { ensurePorfforPatched } from "./patch-porffor";
|
|
17
|
-
import { ensureUWebSockets, ensureUWebSocketsHost, UwsUnavailableError } from "./toolchain";
|
|
17
|
+
import { ensureUWebSockets, ensureUWebSocketsHost, resolveHostCompiler, UwsUnavailableError } from "./toolchain";
|
|
18
18
|
import { ensurePorffor } from "./porffor-toolchain";
|
|
19
19
|
// The prelude + transports live in @sproutboat/runtime (next to wrap.ts, which
|
|
20
20
|
// locates them by file URL). `preludePath`/`transportPath` read them from there
|
|
@@ -109,11 +109,20 @@ export async function loadPrelude(transport: Transport = "broker"): Promise<stri
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/** Child env for the Porffor run. `SB_EXTRA_LINK` is read by the patched link
|
|
112
|
-
* step (#15) and is absent entirely for a normal build.
|
|
113
|
-
|
|
112
|
+
* step (#15) and is absent entirely for a normal build. `cc`/`cxx` override
|
|
113
|
+
* Porffor's own `CC`/`CXX` read for a **host** build (musl builds hardcode
|
|
114
|
+
* `zig cc` themselves and ignore both). */
|
|
115
|
+
function compileEnv(
|
|
116
|
+
path: string,
|
|
117
|
+
extraLink?: string[],
|
|
118
|
+
extraCflags?: string[],
|
|
119
|
+
compiler?: { cc: string[]; cxx: string[] },
|
|
120
|
+
) {
|
|
114
121
|
const link = extraLink && extraLink.length > 0 ? extraLink.join(" ") : undefined;
|
|
115
122
|
const cflags = extraCflags && extraCflags.length > 0 ? extraCflags.join(" ") : undefined;
|
|
116
|
-
|
|
123
|
+
const env = { ...process.env, PATH: path, SB_EXTRA_LINK: link, SB_EXTRA_CFLAGS: cflags };
|
|
124
|
+
if (compiler) return { ...env, CC: compiler.cc.join(" "), CXX: compiler.cxx.join(" ") };
|
|
125
|
+
return env;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
/**
|
|
@@ -130,6 +139,7 @@ export function porfforArgs(
|
|
|
130
139
|
outPath: string,
|
|
131
140
|
target: CompileInput["target"],
|
|
132
141
|
optimize: CompileInput["optimize"],
|
|
142
|
+
usingZig = false,
|
|
133
143
|
): string[] {
|
|
134
144
|
const args = ["native", generatedPath, "-o", outPath];
|
|
135
145
|
if (target !== "host") args.push("--musl");
|
|
@@ -138,6 +148,10 @@ export function porfforArgs(
|
|
|
138
148
|
// three times faster to compile, so dev takes it and everything else does
|
|
139
149
|
// not: a deployable artifact is never built with it.
|
|
140
150
|
if (optimize === "dev" && target === "host") args.push("-O0");
|
|
151
|
+
// Porffor defaults LTO on for a non-musl link. Zig's cc needs `-fuse-ld=lld`
|
|
152
|
+
// wired up for that, which nothing here does, so turn it off rather than
|
|
153
|
+
// fail every host build that falls back to Zig for its C toolchain.
|
|
154
|
+
if (usingZig) args.push("--no-flto");
|
|
141
155
|
return args;
|
|
142
156
|
}
|
|
143
157
|
|
|
@@ -199,8 +213,11 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
199
213
|
);
|
|
200
214
|
|
|
201
215
|
const launcher = resolve(porffor, "runtime/index.js");
|
|
202
|
-
// A
|
|
216
|
+
// A musl build shells `zig` bare (Porffor hardcodes the literal command), so
|
|
217
|
+
// it needs its dir on PATH; a host build passes its compiler via CC/CXX
|
|
218
|
+
// below instead and has no zigBin to contribute here.
|
|
203
219
|
const zigDir = input.zigBin ? `${dirname(input.zigBin)}:` : "";
|
|
220
|
+
const hostCompiler = input.target === "host" ? await resolveHostCompiler() : undefined;
|
|
204
221
|
// The per-platform binary ships esbuild next to it; a compiled build finds it
|
|
205
222
|
// there. Running from the npm package under Bun, `process.execPath` is Bun
|
|
206
223
|
// itself and `npm i -g` puts no dependency `.bin` on PATH, so resolve the
|
|
@@ -221,12 +238,15 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
221
238
|
? [process.execPath, "__porffor", launcher]
|
|
222
239
|
: [process.execPath, resolve(import.meta.dir, "main.ts"), "__porffor", launcher];
|
|
223
240
|
|
|
224
|
-
const child = Bun.spawn(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
241
|
+
const child = Bun.spawn(
|
|
242
|
+
[...command, ...porfforArgs(generatedPath, input.outPath, input.target, input.optimize, hostCompiler?.usingZig)],
|
|
243
|
+
{
|
|
244
|
+
cwd: outDir,
|
|
245
|
+
stdout: "pipe",
|
|
246
|
+
stderr: "pipe",
|
|
247
|
+
env: compileEnv(path, input.extraLink, input.extraCflags, hostCompiler),
|
|
248
|
+
},
|
|
249
|
+
);
|
|
230
250
|
let timedOut = false;
|
|
231
251
|
const timer = setTimeout(() => {
|
|
232
252
|
timedOut = true;
|
package/src/main.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { notifyIfOutdated } from "./update-check";
|
|
|
19
19
|
import { controlVersionWarning } from "./api-version";
|
|
20
20
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { inspectToolchain } from "./toolchain";
|
|
22
23
|
|
|
23
24
|
// A compiled Bun executable is not a general-purpose `bun` command. Porffor
|
|
24
25
|
// is acquired after installation, so run its external ESM launcher through an
|
|
@@ -1194,6 +1195,42 @@ function usage(): never {
|
|
|
1194
1195
|
process.exit(2);
|
|
1195
1196
|
}
|
|
1196
1197
|
|
|
1198
|
+
async function toolchain(args: string[]): Promise<void> {
|
|
1199
|
+
if (args[0] !== "doctor" || args.slice(1).some((arg) => arg !== "--json"))
|
|
1200
|
+
usageError("expected toolchain doctor", "toolchain doctor [--json]");
|
|
1201
|
+
const report = inspectToolchain();
|
|
1202
|
+
if (args.includes("--json")) {
|
|
1203
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
console.log(`host: ${report.host}`);
|
|
1207
|
+
console.log(`cache: ${report.cacheRoot}`);
|
|
1208
|
+
console.log(`Porffor: ${report.porffor.version} (${report.porffor.present ? "ready" : "acquired on first build"})`);
|
|
1209
|
+
console.log(` ${report.porffor.path}${report.porffor.override ? " (override)" : ""}`);
|
|
1210
|
+
console.log(`Zig: ${report.zig.version} (${report.zig.platform ?? "unsupported host"})`);
|
|
1211
|
+
console.log(` ${report.zig.path ?? "set SPROUTBOAT_ZIG to a supported Zig binary"}`);
|
|
1212
|
+
console.log(
|
|
1213
|
+
` ${report.zig.present ? "ready" : "acquired on first deployment build"}${report.zig.override ? " (override)" : ""}`,
|
|
1214
|
+
);
|
|
1215
|
+
console.log(
|
|
1216
|
+
`SQLite: ${report.sqlite.version} (${report.sqlite.present ? "cached" : "acquired for standalone builds"})`,
|
|
1217
|
+
);
|
|
1218
|
+
console.log(` ${report.sqlite.path}`);
|
|
1219
|
+
console.log(
|
|
1220
|
+
`BearSSL: ${report.bearssl.version} (${report.bearssl.present ? "cached" : "acquired for standalone builds"})`,
|
|
1221
|
+
);
|
|
1222
|
+
console.log(` ${report.bearssl.path}`);
|
|
1223
|
+
console.log(
|
|
1224
|
+
`host compiler: ${report.prerequisites.cc ?? "missing: install Xcode Command Line Tools or a C compiler"}`,
|
|
1225
|
+
);
|
|
1226
|
+
console.log(`archiver: ${report.prerequisites.ar ?? "missing: install an archiver"}`);
|
|
1227
|
+
console.log(`archive extractor: ${report.prerequisites.tar ?? "missing: install tar with xz support"}`);
|
|
1228
|
+
if (process.platform === "darwin")
|
|
1229
|
+
console.log(`macOS SDK tools: ${report.prerequisites.xcrun ?? "missing: run xcode-select --install"}`);
|
|
1230
|
+
if (process.platform === "darwin")
|
|
1231
|
+
console.log(`macOS SDK: ${report.prerequisites.sdk ?? "missing: run xcode-select --install"}`);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1197
1234
|
const [command, ...args] = process.argv.slice(2);
|
|
1198
1235
|
if (command === undefined || command === "help" || command === "-h" || command === "--help") help();
|
|
1199
1236
|
if (command === "--version" || command === "-v") {
|
|
@@ -1223,6 +1260,9 @@ switch (command) {
|
|
|
1223
1260
|
else await build(directory, hostBuild ? "host" : "linux-x86_64");
|
|
1224
1261
|
break;
|
|
1225
1262
|
}
|
|
1263
|
+
case "toolchain":
|
|
1264
|
+
await toolchain(args);
|
|
1265
|
+
break;
|
|
1226
1266
|
case "login":
|
|
1227
1267
|
await login(args);
|
|
1228
1268
|
break;
|