sproutboat 0.51.0 → 0.52.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 +34 -0
- package/SURFACE.md +3 -1
- package/package.json +8 -8
- package/src/bearssl.ts +19 -6
- package/src/build.ts +10 -2
- package/src/compile.ts +127 -0
- package/src/fetch-pinned.ts +80 -0
- package/src/surface.ts +9 -0
- package/src/toolchain.ts +10 -2
- package/vendor/bearssl/LICENSE.txt +21 -0
- package/vendor/bearssl/README.md +14 -0
- package/vendor/bearssl/bearssl-7bea48e.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,40 @@ maintained going forward by the `release` skill.
|
|
|
10
10
|
|
|
11
11
|
## Unreleased
|
|
12
12
|
|
|
13
|
+
## [0.52.0] - 2026-11-06
|
|
14
|
+
|
|
15
|
+
Built on `@sproutboat/toolchain` 0.24.1, `@sproutboat/runtime` 0.42.2 and `@sproutboat/wire` 0.22.2. The compiler changes, so every app is recompiled on its next build. Nothing an app can observe changes apart from the two fixes below, so there is no new compatibility date.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Standalone binaries and deploys now build with Porffor alpha 22** (was alpha 19). Binaries are about 5% smaller (hello 6.58 to 6.27 MB, stress 6.62 to 6.30 MB, linux-x64). Apps using jose, yaml, supabase-js and Neon (HTTP, and `Pool` over WebSocket) are checked route by route and give the same results as on alpha 19 and on Bun. Measured against alpha 19 on the same box: idle memory is 3 to 7% lower, requests per second are 5 to 15% lower, p50 latency is 3 to 8% higher, and p95 is up to about 30% (5 to 8 µs) higher on a request-heavy app. The module is composed the way sproutd's workers are, so no Node runtime module is linked, and it is served by the same uWebSockets server as before (WebSockets, streaming, graceful restart). LTO is on for optimized builds.
|
|
20
|
+
- **Fallback:** `SPROUTBOAT_PORFFOR_CHANNEL=alpha-19` builds with the previous alpha 19 compiler for this release only; it will be removed in the next one. The compile cache is keyed by channel, so switching back and forth does not reuse the wrong binary. The artifact manifest and `sproutboat versions view` now report the Porffor release a build actually used (`alpha-22 (a081ebd)`).
|
|
21
|
+
- **`build --standalone --with-restore` stays on alpha 19 for now.** Its offline `ltx-restore` command is built against alpha 19's native C API, which alpha 22 does not provide yet, so such a build uses the alpha 19 compiler. Setting `SPROUTBOAT_PORFFOR_CHANNEL=alpha-22` together with `--with-restore` is an error, not a link failure.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A `Uint8Array` or `ArrayBuffer` response body is sent with its real length.** On the alpha 22 server the bridge read the length from the buffer's flags word, so such a response went out with `Content-Length: 2147483647`. Found while qualifying alpha 22. No release ever shipped it, since alpha 19 was the default until now.
|
|
26
|
+
- **Neon's `Pool` over WebSocket no longer hangs.** Alpha 22 linked its Node runtime's `process`, `setImmediate`, `clearImmediate` and `global` for any app that named them. pg-pool then queued on `process.nextTick`, and a standalone binary never drains that queue. These four names are now absent, as on alpha 19, so libraries no longer take their Node code paths.
|
|
27
|
+
|
|
28
|
+
## [0.51.1] - 2026-11-05
|
|
29
|
+
|
|
30
|
+
Built on `@sproutboat/runtime` 0.42.2, `@sproutboat/toolchain` 0.23.1 and `@sproutboat/wire` 0.22.2. Nothing an app can observe changes except latency, the refused address ranges below and the `startTls()` write order, so there is no new compatibility date.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Bytes written right after `socket.startTls()` no longer go out unencrypted.** On a `connect()` socket, the I/O thread took the start-TLS request and the app's queued writes in the same pass, and flushed all of them in plaintext before switching to TLS. Any write queued before the thread picked up the upgrade could leave in the clear. Only the bytes written before `startTls()` now go out in plaintext. Everything after waits until the TLS engine owns the socket, and new writes on the old plaintext socket reject, as in workerd. A failed plaintext flush is now a connection error. The egress tunnel also no longer stays paused on the discarded plain socket after an upgrade under backpressure.
|
|
35
|
+
|
|
36
|
+
- The BearSSL source ships in the package (`vendor/bearssl`, 867 KB) and is read first, hash-verified, so builds no longer depend on bearssl.org being up. bearssl.org stays as the fallback, with bounded retry on 5xx. Every source, vendored or downloaded, is checked against the pinned sha256, so a bad file cannot change what gets built.
|
|
37
|
+
- **A malformed `connect()` tunnel opening frame no longer ends the egress process.** A `tcp.open` head whose JSON did not parse used to throw out of the egress service's socket handler before the token was checked, which took egress down for every app. It now gets an `expected tcp.open` error frame and only that connection closes; the broker answers it as a bad request, and a refused head stops buffering whatever the client keeps sending.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`fetch()` and `connect()` refuse more reserved address space** that carries no public service: AS112 and the old IPv4 anycast blocks (`192.31.196.0/24`, `192.52.193.0/24`, `192.88.99.0/24`, `192.175.48.0/24`), SIIT (`::ffff:0:0:0/96`), IETF protocol assignments including Teredo (`2001::/23`), `3fff::/20` and SRv6 (`5f00::/16`). This applies to the broker, the egress service and a standalone binary alike, and `SB_EGRESS_ALLOW` still overrides it.
|
|
42
|
+
|
|
43
|
+
### Performance
|
|
44
|
+
|
|
45
|
+
- **Outbound TCP sockets set `TCP_NODELAY`.** Nagle's algorithm and the peer's delayed ACK stalled every fresh HTTPS `fetch()` by about 44 ms on Linux, because each small TLS handshake flight waited for an ACK; a fresh fetch now takes about 1.4 ms. This covers `fetch()`, outbound WebSockets and `connect()` on a standalone binary, including sockets handed over by `SB_EGRESS_FD`, and the egress tunnel's connections to origins.
|
|
46
|
+
|
|
13
47
|
## [0.51.0] - 2026-11-03
|
|
14
48
|
|
|
15
49
|
Built on `@sproutboat/runtime` 0.42.0, `@sproutboat/toolchain` 0.23.0 and `@sproutboat/wire` 0.22.0. The change below starts at a new compatibility date; earlier dates answer as before.
|
package/SURFACE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
|
|
4
4
|
> toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
|
|
5
5
|
|
|
6
|
-
**Package:** `sproutboat` 0.
|
|
6
|
+
**Package:** `sproutboat` 0.52.0
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -48,6 +48,8 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
48
48
|
| `CXX` | C++ compiler for a host build's native-fetch link step (default `c++` if present, else the vendored Zig cross-compiler targeting this host). |
|
|
49
49
|
| `AR` | Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's). |
|
|
50
50
|
| `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
|
|
51
|
+
| `SPROUTBOAT_PORFFOR_CHANNEL` | Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release). |
|
|
52
|
+
| `SPROUTBOAT_FLTO` | Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off. |
|
|
51
53
|
| `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
|
|
52
54
|
| `SPROUTBOAT_PORFFOR_DIR` | Contributor override for an existing Porffor source checkout. Required files are validated before use. |
|
|
53
55
|
| `SPROUTBOAT_BUILD_UWS_FROM_SOURCE` | Set to 1 to explicitly allow the contributor-only Git and Make uWebSockets fallback. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@sproutboat/artifact": "^0.3.0",
|
|
65
65
|
"@sproutboat/assets": "^0.3.0",
|
|
66
66
|
"@sproutboat/config": "^0.6.0",
|
|
67
|
-
"@sproutboat/runtime": "^0.42.
|
|
68
|
-
"@sproutboat/toolchain": "^0.
|
|
69
|
-
"@sproutboat/wire": "^0.22.
|
|
67
|
+
"@sproutboat/runtime": "^0.42.2",
|
|
68
|
+
"@sproutboat/toolchain": "^0.24.1",
|
|
69
|
+
"@sproutboat/wire": "^0.22.2"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@oxlint/plugins": "1.85.0",
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"typescript": "7.0.2"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "0.
|
|
81
|
+
"@sproutboat/cli-darwin-arm64": "0.52.0",
|
|
82
|
+
"@sproutboat/cli-darwin-x64": "0.52.0",
|
|
83
|
+
"@sproutboat/cli-linux-arm64": "0.52.0",
|
|
84
|
+
"@sproutboat/cli-linux-x64": "0.52.0"
|
|
85
85
|
},
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18"
|
package/src/bearssl.ts
CHANGED
|
@@ -19,6 +19,9 @@ import { existsSync } from "node:fs";
|
|
|
19
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
|
+
// @ts-expect-error Bun's file loader returns the embedded asset path in a compiled executable.
|
|
23
|
+
import embeddedBearsslArchive from "../vendor/bearssl/bearssl-7bea48e.tar.gz" with { type: "file" };
|
|
24
|
+
import { fetchPinned, type FetchPinnedOptions } from "./fetch-pinned";
|
|
22
25
|
import { LINUX_TARGETS, type BuildTarget } from "./targets";
|
|
23
26
|
import { resolveHostCompiler } from "./toolchain";
|
|
24
27
|
|
|
@@ -112,12 +115,22 @@ const run = (
|
|
|
112
115
|
child.on("close", (code) => done({ code: code ?? 1, stderr }));
|
|
113
116
|
});
|
|
114
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The pinned BearSSL source: the vendored copy (ships in the npm package, embedded in standalone
|
|
120
|
+
* binaries) first, bearssl.org only as a fallback. bearssl.org returned 502 for hours on 2026-11-04.
|
|
121
|
+
*/
|
|
122
|
+
export const bearsslVendorArchive = (): string => embeddedBearsslArchive;
|
|
123
|
+
|
|
124
|
+
export const fetchBearsslTarball = (overrides: Partial<FetchPinnedOptions> = {}): Promise<Buffer> =>
|
|
125
|
+
fetchPinned({
|
|
126
|
+
localPath: bearsslVendorArchive(),
|
|
127
|
+
urls: [BEARSSL_URL],
|
|
128
|
+
sha256: BEARSSL_SHA256,
|
|
129
|
+
...overrides,
|
|
130
|
+
});
|
|
131
|
+
|
|
115
132
|
async function download(url: string, expected: string, to: string): Promise<Buffer> {
|
|
116
|
-
const
|
|
117
|
-
if (!response.ok) throw new Error(`could not download ${url}: HTTP ${response.status}`);
|
|
118
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
119
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
120
|
-
if (digest !== expected) throw new Error(`checksum mismatch for ${url}: expected ${expected}, got ${digest}`);
|
|
133
|
+
const bytes = await fetchPinned({ urls: [url], sha256: expected });
|
|
121
134
|
await writeFile(to, bytes);
|
|
122
135
|
return bytes;
|
|
123
136
|
}
|
|
@@ -143,7 +156,7 @@ async function sourceTree(): Promise<string> {
|
|
|
143
156
|
await rm(tree, { recursive: true, force: true });
|
|
144
157
|
if (!existsSync(tarball) || (await digest(tarball)) !== BEARSSL_SHA256) {
|
|
145
158
|
await rm(tarball, { force: true });
|
|
146
|
-
await
|
|
159
|
+
await writeFile(tarball, await fetchBearsslTarball());
|
|
147
160
|
}
|
|
148
161
|
const stage = resolve(dir, `.bearssl-stage-${process.pid}-${crypto.randomUUID()}`);
|
|
149
162
|
await mkdir(stage, { recursive: true });
|
package/src/build.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { queueSettings, resourceRefs, type SproutboatConfig } from "./config";
|
|
|
7
7
|
import { ensureSqliteObject } from "./sqlite";
|
|
8
8
|
import { sqliteStamp } from "./sqlite";
|
|
9
9
|
import { bearsslStamp, ensureBearssl } from "./bearssl";
|
|
10
|
-
import { compileSprout, loadPrelude, type Transport } from "./compile";
|
|
10
|
+
import { compileSprout, loadPrelude, porfforChannel, type Transport } from "./compile";
|
|
11
11
|
import { compileCached } from "./compile-cache";
|
|
12
12
|
import type { BuildTarget } from "./targets";
|
|
13
13
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./manifest";
|
|
21
21
|
import { ensureZig, esbuildVersion, porfforVersion, toolchainStamp } from "./toolchain";
|
|
22
22
|
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_COMMIT_FULL } from "./porffor-toolchain";
|
|
23
|
+
import { A21_STANDALONE_FINGERPRINT } from "@sproutboat/toolchain/a21";
|
|
23
24
|
import { version as toolchainPackageVersion } from "@sproutboat/toolchain/package.json" with { type: "json" };
|
|
24
25
|
import { version as cliVersion } from "../package.json" with { type: "json" };
|
|
25
26
|
import { reachesSubtle, wrapNativeFetchHandler } from "./wrap";
|
|
@@ -336,6 +337,13 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
336
337
|
toolchain: toolchainStamp(),
|
|
337
338
|
toolchainPackageVersion,
|
|
338
339
|
porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
|
|
340
|
+
// the alpha 22 channel is a different compiler, server and prelude shape (compile.ts porfforChannel)
|
|
341
|
+
channel: porfforChannel(input.withRestore),
|
|
342
|
+
// the alpha 22 channel's own inputs: the patched tree, the shim, the bridge, the build recipe, LTO
|
|
343
|
+
channelInputs:
|
|
344
|
+
porfforChannel(input.withRestore) === "alpha-22"
|
|
345
|
+
? [A21_STANDALONE_FINGERPRINT, process.env.SPROUTBOAT_FLTO ?? ""]
|
|
346
|
+
: null,
|
|
339
347
|
native: embedded ? [sqliteStamp(), bearsslStamp()] : subtle ? [bearsslStamp()] : [],
|
|
340
348
|
integration: await integrationIdentity(),
|
|
341
349
|
nativeRestore: input.withRestore ? NATIVE_RESTORE_IDENTITY : null,
|
|
@@ -427,7 +435,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
427
435
|
target: host ? hostTarget() : target === "linux-arm64" ? target : DEPLOY_TARGET,
|
|
428
436
|
runtime: RUNTIME,
|
|
429
437
|
capabilityProfile: CAPABILITY_PROFILE,
|
|
430
|
-
porfforVersion: porfforVersion(),
|
|
438
|
+
porfforVersion: porfforVersion(porfforChannel(input.withRestore)),
|
|
431
439
|
esbuildVersion: esbuildVersion(),
|
|
432
440
|
buildImage: toolchainStamp(),
|
|
433
441
|
compatibilityDate: input.config.compatibility_date,
|
package/src/compile.ts
CHANGED
|
@@ -20,10 +20,20 @@ import {
|
|
|
20
20
|
ensureUWebSocketsHost,
|
|
21
21
|
ensureUWebSocketsStandalone,
|
|
22
22
|
resolveHostCompiler,
|
|
23
|
+
uwsCommitFull,
|
|
23
24
|
UwsUnavailableError,
|
|
24
25
|
} from "./toolchain";
|
|
25
26
|
import { isLinuxTarget, LINUX_TARGETS, linuxTriple, type BuildTarget } from "./targets";
|
|
26
27
|
import { ensurePorffor } from "./porffor-toolchain";
|
|
28
|
+
import {
|
|
29
|
+
buildStandaloneA21,
|
|
30
|
+
composeStandaloneA21,
|
|
31
|
+
ensurePorfforA21,
|
|
32
|
+
SHARED_PRELUDE_FILE,
|
|
33
|
+
STANDALONE_A21_COMPILE_FLAGS,
|
|
34
|
+
standaloneChannel,
|
|
35
|
+
type StandaloneChannel,
|
|
36
|
+
} from "@sproutboat/toolchain/a21";
|
|
27
37
|
// The prelude + transports live in @sproutboat/runtime (next to wrap.ts, which
|
|
28
38
|
// locates them by file URL). `preludePath`/`transportPath` read them from there
|
|
29
39
|
// for a dev build; these text imports bake the same files into the `--compile`
|
|
@@ -56,6 +66,29 @@ export {
|
|
|
56
66
|
|
|
57
67
|
const COMPILE_TIMEOUT_MS = Number(process.env.SPROUTBOAT_COMPILE_TIMEOUT_MS || 600_000);
|
|
58
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The Porffor channel a native compile uses: `alpha-22` by default (the alpha 21 line's compiler with
|
|
71
|
+
* the toolchain's alpha 19 server shim over its bridge; sproutd-alpha22-standalone-port.md), or
|
|
72
|
+
* `alpha-19` (`SPROUTBOAT_PORFFOR_CHANNEL=alpha-19`: Porffor's native-fetch server, the fallback pin
|
|
73
|
+
* kept for one release). The toolchain's `standaloneChannel()` decides, so the CLI and the runtime's
|
|
74
|
+
* native tests agree on the default.
|
|
75
|
+
*
|
|
76
|
+
* `--with-restore` stays on alpha 19: its restore objects are built against alpha 19's native-fetch
|
|
77
|
+
* C API, which the alpha 22 bridge does not provide, so they do not link there. Unless the channel is
|
|
78
|
+
* set explicitly, such a build uses the alpha 19 compiler; `SPROUTBOAT_PORFFOR_CHANNEL=alpha-22` with
|
|
79
|
+
* `--with-restore` is an error rather than a link failure.
|
|
80
|
+
*/
|
|
81
|
+
export type PorfforChannel = StandaloneChannel;
|
|
82
|
+
export function porfforChannel(withRestore = false): PorfforChannel {
|
|
83
|
+
const channel = standaloneChannel();
|
|
84
|
+
if (!withRestore || channel === "alpha-19") return channel;
|
|
85
|
+
if (process.env.SPROUTBOAT_PORFFOR_CHANNEL)
|
|
86
|
+
throw new Error(
|
|
87
|
+
"--with-restore is not available on Porffor alpha 22 yet: unset SPROUTBOAT_PORFFOR_CHANNEL or set it to alpha-19",
|
|
88
|
+
);
|
|
89
|
+
return "alpha-19";
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
export type CompileInput = {
|
|
60
93
|
sourcePath: string;
|
|
61
94
|
/** Stable compiler input for dev rebuilds, independent of the candidate binary path. */
|
|
@@ -334,6 +367,22 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
334
367
|
})
|
|
335
368
|
: [];
|
|
336
369
|
|
|
370
|
+
if (porfforChannel(input.withRestore) === "alpha-22") {
|
|
371
|
+
await compileSproutA21(input, {
|
|
372
|
+
alpha19Root: porffor,
|
|
373
|
+
generatedPath,
|
|
374
|
+
launch: PACKAGED
|
|
375
|
+
? [process.execPath, "__porffor"]
|
|
376
|
+
: [process.execPath, resolve(import.meta.dir, "main.ts"), "__porffor"],
|
|
377
|
+
path,
|
|
378
|
+
hostCompiler,
|
|
379
|
+
restoreObjects,
|
|
380
|
+
uwsVariant,
|
|
381
|
+
});
|
|
382
|
+
await chmod(input.outPath, 0o555);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
337
386
|
const child = Bun.spawn(
|
|
338
387
|
[...command, ...porfforArgs(generatedPath, input.outPath, input.target, input.optimize, hostCompiler?.usingZig)],
|
|
339
388
|
{
|
|
@@ -369,3 +418,81 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
369
418
|
}
|
|
370
419
|
await chmod(input.outPath, 0o555);
|
|
371
420
|
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The alpha 22 channel's native compile (sproutd-alpha22-standalone-port.md): the generated module is
|
|
424
|
+
* compiled in sproutd's worker shape (the toolchain composes `shared-prelude.js` from it: alpha 19
|
|
425
|
+
* helper names, the alpha 19 web classes, the dispatch shim), the alpha 22 compiler renders the split
|
|
426
|
+
* C units, and the toolchain's build recipe compiles them with the native-fetch bridge and the alpha
|
|
427
|
+
* 19 uWebSockets shim rewritten over flat data, then links with this build's native objects (SQLite,
|
|
428
|
+
* BearSSL, the restore objects) and the uSockets this target already has. Same inputs, same outputs,
|
|
429
|
+
* same environment variables as the alpha 19 path; the only switch is `SPROUTBOAT_PORFFOR_CHANNEL`.
|
|
430
|
+
*/
|
|
431
|
+
async function compileSproutA21(
|
|
432
|
+
input: CompileInput,
|
|
433
|
+
build: {
|
|
434
|
+
alpha19Root: string;
|
|
435
|
+
generatedPath: string;
|
|
436
|
+
launch: string[];
|
|
437
|
+
path: string;
|
|
438
|
+
hostCompiler?: { cc: string[]; cxx: string[]; usingZig: boolean };
|
|
439
|
+
restoreObjects: string[];
|
|
440
|
+
uwsVariant?: string;
|
|
441
|
+
},
|
|
442
|
+
): Promise<void> {
|
|
443
|
+
const alpha22 = await ensurePorfforA21("alpha-22");
|
|
444
|
+
const dir = dirname(build.generatedPath);
|
|
445
|
+
const composed = composeStandaloneA21(await readFile(build.generatedPath, "utf8"), build.alpha19Root);
|
|
446
|
+
// beside sprout.generated.js (which stays the wrapped module, for emit-only, dev and the cache key)
|
|
447
|
+
await writeFile(resolve(dir, SHARED_PRELUDE_FILE), composed.sharedPrelude);
|
|
448
|
+
const entry = resolve(dir, "sprout.a22.js");
|
|
449
|
+
await writeFile(entry, composed.entry);
|
|
450
|
+
const unitsDir = resolve(dir, "units-a22");
|
|
451
|
+
await rm(unitsDir, { recursive: true, force: true }).catch(() => {});
|
|
452
|
+
// the compiler's own build cache (keyed by entry path) is not used: `c` only renders
|
|
453
|
+
const render = Bun.spawn(
|
|
454
|
+
[
|
|
455
|
+
...build.launch,
|
|
456
|
+
resolve(alpha22, "runtime/index.js"),
|
|
457
|
+
"c",
|
|
458
|
+
entry,
|
|
459
|
+
"-o",
|
|
460
|
+
`${unitsDir}/`,
|
|
461
|
+
...STANDALONE_A21_COMPILE_FLAGS,
|
|
462
|
+
],
|
|
463
|
+
{ cwd: dir, stdout: "pipe", stderr: "pipe", env: { ...process.env, PATH: build.path } },
|
|
464
|
+
);
|
|
465
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
466
|
+
render.exited,
|
|
467
|
+
new Response(render.stdout).text(),
|
|
468
|
+
new Response(render.stderr).text(),
|
|
469
|
+
]);
|
|
470
|
+
if (code !== 0) throw new Error(`Porffor (alpha 22) render failed: ${(stderr || stdout).trim() || `exit ${code}`}`);
|
|
471
|
+
|
|
472
|
+
const host = input.target === "host";
|
|
473
|
+
const triple = host ? undefined : linuxTriple(input.target);
|
|
474
|
+
const cc = host ? build.hostCompiler!.cc : [input.zigBin!, "cc", "-target", triple!];
|
|
475
|
+
const cxx = host ? build.hostCompiler!.cxx : [input.zigBin!, "c++", "-target", triple!];
|
|
476
|
+
const uwsDir = resolve(
|
|
477
|
+
homedir(),
|
|
478
|
+
".cache/porffor/deps",
|
|
479
|
+
host ? `uWebSockets-${uwsCommitFull()}` : `uWebSockets-${uwsCommitFull()}-musl-${build.uwsVariant ?? "sa-x86_64"}`,
|
|
480
|
+
);
|
|
481
|
+
await buildStandaloneA21({
|
|
482
|
+
unitsDir,
|
|
483
|
+
alpha19Root: build.alpha19Root,
|
|
484
|
+
outPath: input.outPath,
|
|
485
|
+
cc,
|
|
486
|
+
cxx,
|
|
487
|
+
uwsDir,
|
|
488
|
+
extraLink: [...(input.extraLink ?? []), ...build.restoreObjects],
|
|
489
|
+
extraCflags: [...(input.extraCflags ?? []), ...(input.withRestore ? ["-Dmain=sb_app_main"] : [])],
|
|
490
|
+
optimize: input.optimize === "dev" && host ? 0 : 3,
|
|
491
|
+
// LTO on for every optimized build, zig's musl cross builds included (romeo, M4: +11% req/s on hello,
|
|
492
|
+
// +7% on stress, and 4% smaller); SPROUTBOAT_FLTO=0 turns it off for measurements
|
|
493
|
+
flto: process.env.SPROUTBOAT_FLTO ? process.env.SPROUTBOAT_FLTO === "1" : !(input.optimize === "dev" && host),
|
|
494
|
+
musl: !host,
|
|
495
|
+
strip: true,
|
|
496
|
+
});
|
|
497
|
+
if (!(await Bun.file(input.outPath).exists())) throw new Error("alpha 22 build produced no binary");
|
|
498
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
type Fetcher = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
7
|
+
|
|
8
|
+
export type FetchPinnedOptions = {
|
|
9
|
+
/** A vendored copy read from disk before any network source. Used only if its hash matches. */
|
|
10
|
+
localPath?: string;
|
|
11
|
+
/** Ordered download sources, tried after the vendored copy. Every one must serve the same bytes. */
|
|
12
|
+
urls: readonly string[];
|
|
13
|
+
/** Lowercase hex sha256 of the exact bytes. A source that serves anything else is rejected. */
|
|
14
|
+
sha256: string;
|
|
15
|
+
/** A verified copy is kept here (written atomically) and reused while its hash matches. */
|
|
16
|
+
cachePath?: string;
|
|
17
|
+
fetcher?: Fetcher;
|
|
18
|
+
/** Attempts per source for a 5xx or a network error. 4xx and a hash mismatch are never retried. */
|
|
19
|
+
attempts?: number;
|
|
20
|
+
/** First backoff; doubles each retry. */
|
|
21
|
+
backoffMs?: number;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sha256Hex = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
|
|
28
|
+
/** A failure that will not change on retry: a 4xx, or bytes that fail the pin. */
|
|
29
|
+
class Permanent extends Error {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return a build-time dependency's bytes, only if they hash to the pinned sha256: first the vendored
|
|
33
|
+
* file (`localPath`), then the ordered download list. The pin is what makes every source safe: one
|
|
34
|
+
* that has different bytes (a tampered vendored file included) is skipped, so the list can only
|
|
35
|
+
* improve availability, never change what gets built. A 5xx or a network error is retried with exponential backoff (bounded); a 4xx or a
|
|
36
|
+
* hash mismatch moves straight on to the next source.
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchPinned(options: FetchPinnedOptions): Promise<Buffer> {
|
|
39
|
+
const sha256 = options.sha256.toLowerCase();
|
|
40
|
+
const fetcher: Fetcher = options.fetcher ?? fetch;
|
|
41
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
42
|
+
const sleep = options.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
43
|
+
if (options.localPath && existsSync(options.localPath)) {
|
|
44
|
+
const vendored = await readFile(options.localPath);
|
|
45
|
+
if (sha256Hex(vendored) === sha256) return vendored;
|
|
46
|
+
console.warn(
|
|
47
|
+
`fetchPinned: vendored ${options.localPath} does not match the pinned sha256, falling back to download`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (options.cachePath && existsSync(options.cachePath)) {
|
|
51
|
+
const cached = await readFile(options.cachePath);
|
|
52
|
+
if (sha256Hex(cached) === sha256) return cached;
|
|
53
|
+
await rm(options.cachePath, { force: true });
|
|
54
|
+
}
|
|
55
|
+
const failures: string[] = [];
|
|
56
|
+
for (const url of options.urls) {
|
|
57
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
58
|
+
if (attempt > 0) await sleep((options.backoffMs ?? 500) * 2 ** (attempt - 1));
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetcher(url, { signal: AbortSignal.timeout(options.timeoutMs ?? 60_000) });
|
|
61
|
+
if (response.status >= 500) throw new Error(`HTTP ${response.status}`);
|
|
62
|
+
if (!response.ok) throw new Permanent(`HTTP ${response.status}`);
|
|
63
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
64
|
+
const got = sha256Hex(bytes);
|
|
65
|
+
if (got !== sha256) throw new Permanent(`sha256 mismatch (expected ${sha256}, got ${got})`);
|
|
66
|
+
if (options.cachePath) {
|
|
67
|
+
await mkdir(dirname(options.cachePath), { recursive: true });
|
|
68
|
+
const stage = `${options.cachePath}.${process.pid}.tmp`;
|
|
69
|
+
await writeFile(stage, bytes);
|
|
70
|
+
await rename(stage, options.cachePath);
|
|
71
|
+
}
|
|
72
|
+
return bytes;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
+
if (error instanceof Permanent) break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`could not download a verified copy (sha256 ${sha256}) from any source:\n ${failures.join("\n ")}`);
|
|
80
|
+
}
|
package/src/surface.ts
CHANGED
|
@@ -237,6 +237,15 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
237
237
|
purpose: "Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's).",
|
|
238
238
|
},
|
|
239
239
|
{ name: "SPROUTBOAT_COMPILE_TIMEOUT_MS", purpose: "Porffor compile timeout in ms (default 600000)." },
|
|
240
|
+
{
|
|
241
|
+
name: "SPROUTBOAT_PORFFOR_CHANNEL",
|
|
242
|
+
purpose:
|
|
243
|
+
"Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release).",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "SPROUTBOAT_FLTO",
|
|
247
|
+
purpose: "Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off.",
|
|
248
|
+
},
|
|
240
249
|
{
|
|
241
250
|
name: "SPROUTBOAT_TOOLCHAIN_CACHE",
|
|
242
251
|
purpose: "Managed Porffor and compiler cache root (default ~/.cache/sproutboat).",
|
package/src/toolchain.ts
CHANGED
|
@@ -16,6 +16,12 @@ 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
18
|
import { BEARSSL_VERSION } from "./bearssl";
|
|
19
|
+
import {
|
|
20
|
+
PORFFOR_A22_CHANNEL,
|
|
21
|
+
PORFFOR_A22_COMMIT,
|
|
22
|
+
standaloneChannel,
|
|
23
|
+
type StandaloneChannel,
|
|
24
|
+
} from "@sproutboat/toolchain/a21";
|
|
19
25
|
import { cachedPorfforRoot, PORFFOR_CHANNEL, PORFFOR_COMMIT } from "./porffor-toolchain";
|
|
20
26
|
import { SQLITE_VERSION } from "./sqlite";
|
|
21
27
|
import { LINUX_TARGETS, type LinuxTarget } from "./targets";
|
|
@@ -443,7 +449,7 @@ export function inspectToolchain(): ToolchainDoctor {
|
|
|
443
449
|
};
|
|
444
450
|
}
|
|
445
451
|
|
|
446
|
-
function uwsCommitFull(): string {
|
|
452
|
+
export function uwsCommitFull(): string {
|
|
447
453
|
try {
|
|
448
454
|
const src = readFileSync(resolve(porfforRoot(), "compiler/uwebsockets.js"), "utf8");
|
|
449
455
|
return /UWS_COMMIT\s*=\s*['"]([0-9a-f]{40})/.exec(src)?.[1] ?? UWS_COMMIT_FULL;
|
|
@@ -672,8 +678,10 @@ export function porfforRoot(start = import.meta.dir): string {
|
|
|
672
678
|
}
|
|
673
679
|
}
|
|
674
680
|
|
|
675
|
-
|
|
681
|
+
/** The Porffor release a build compiles with, on the standalone channel it uses (default: the environment's). */
|
|
682
|
+
export function porfforVersion(channel: StandaloneChannel = standaloneChannel()): string {
|
|
676
683
|
if (process.env.PORFFOR_VERSION) return process.env.PORFFOR_VERSION;
|
|
684
|
+
if (channel === "alpha-22") return `${PORFFOR_A22_CHANNEL} (${PORFFOR_A22_COMMIT})`;
|
|
677
685
|
return `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
|
|
678
686
|
}
|
|
679
687
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
18
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
19
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Vendored BearSSL source
|
|
2
|
+
|
|
3
|
+
`bearssl-7bea48e.tar.gz` is the BearSSL source snapshot the CLI builds embedded sprouts against. It ships in the
|
|
4
|
+
npm package (and is embedded in standalone binaries), so a build needs no network access to bearssl.org, which
|
|
5
|
+
returned HTTP 502 for hours on 2026-11-04.
|
|
6
|
+
|
|
7
|
+
- Origin: https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=7bea48e5e850ab4cafbe68d3765cdaba13a86d6f;sf=tgz
|
|
8
|
+
- Commit: `7bea48e5e850ab4cafbe68d3765cdaba13a86d6f` (`7bea48e5`; `git get-tar-commit-id` agrees)
|
|
9
|
+
- sha256: `ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372`
|
|
10
|
+
- License: MIT, see `LICENSE.txt` (copied from the tarball)
|
|
11
|
+
|
|
12
|
+
`src/bearssl.ts` pins the sha256 and `fetchPinned` (`src/fetch-pinned.ts`) rejects this file if it does not match,
|
|
13
|
+
then falls back to bearssl.org. To update: replace the tarball, then change `BEARSSL_COMMIT`, `BEARSSL_SHA256` and
|
|
14
|
+
the import path in `src/bearssl.ts`.
|
|
Binary file
|