sproutboat 0.11.11 → 0.11.13
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 +35 -2
- package/SURFACE.md +2 -2
- package/THIRD_PARTY_NOTICES.md +3 -5
- package/package.json +9 -10
- package/scripts/build-cli.ts +2 -44
- package/src/bearssl.ts +2 -1
- package/src/build.ts +208 -118
- package/src/bundle.ts +31 -3
- package/src/compile-cache.ts +98 -0
- package/src/compile.ts +28 -37
- package/src/dev.ts +10 -0
- package/src/main.ts +1 -0
- package/src/node-shims.ts +378 -0
- package/src/standalone-build.ts +4 -14
- package/src/toolchain.ts +3 -8
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,38 @@ a breaking change.
|
|
|
8
8
|
Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
|
|
9
9
|
maintained going forward by the `release` skill.
|
|
10
10
|
|
|
11
|
+
## Unreleased
|
|
12
|
+
|
|
13
|
+
## [0.11.13] - 2026-09-26
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Record compile duration in the artifact manifest so the platform can show it per deployment.
|
|
17
|
+
- Support selected `node:path`, `node:url`, and `node:querystring` APIs in handlers.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Reuse checksum-verified native binaries for identical release builds and
|
|
21
|
+
refresh broker asset sidecars without recompiling the handler.
|
|
22
|
+
- Report native compilation time and binary cache status after `build`.
|
|
23
|
+
- Reject unsupported service bindings before starting a standalone build.
|
|
24
|
+
- Fetch the pinned Mozilla CA bundle from curl's dated archive so new upstream
|
|
25
|
+
revisions do not break fresh standalone builds.
|
|
26
|
+
- Update the pinned toolchain to `0.4.13` and the artifact contract to `0.3.0`.
|
|
27
|
+
|
|
28
|
+
## [0.11.12] - 2026-09-24
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Porffor alpha 9 handles ESM modules without the external esbuild executable.
|
|
32
|
+
The CLI now requires `@sproutboat/toolchain@^0.4.12`, runtime `^0.12.1`,
|
|
33
|
+
and wire `^0.9.1`.
|
|
34
|
+
Removed the CLI's esbuild dependency, platform binary copies, and compile-time
|
|
35
|
+
PATH lookup. Handler validation and shimming still use Bun's built-in bundler.
|
|
36
|
+
The required `esbuildVersion` manifest field remains for older artifact
|
|
37
|
+
readers and now records `unused`.
|
|
38
|
+
- Local `dev` rebuilds keep a stable generated entry path for each process, so
|
|
39
|
+
Porffor alpha 9 can reuse unchanged compiled C units. In a local `hello`
|
|
40
|
+
handler-edit comparison, compilation took 549 ms with the stable path versus
|
|
41
|
+
1,505 ms with changing paths. Candidate binaries remain isolated.
|
|
42
|
+
|
|
11
43
|
## [0.11.11] - 2026-09-24
|
|
12
44
|
### Fixed
|
|
13
45
|
- Updated the native runtime to `0.12.0` so repeated HMAC signing uses the raw
|
|
@@ -15,7 +47,6 @@ maintained going forward by the `release` skill.
|
|
|
15
47
|
- Updated the toolchain to `0.4.11` so `Uint8Array.from({ length: 16 }, mapFn)`
|
|
16
48
|
produces the intended key bytes in standalone native builds.
|
|
17
49
|
- Added the two-round HMAC vector to broker and standalone conformance checks.
|
|
18
|
-
|
|
19
50
|
## [0.11.10] - 2026-09-18
|
|
20
51
|
### Changed
|
|
21
52
|
- Bumped `@sproutboat/toolchain` to `0.4.10`, pinning Porffor to the
|
|
@@ -596,7 +627,9 @@ its own package.
|
|
|
596
627
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
597
628
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
598
629
|
|
|
599
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.
|
|
630
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.13...HEAD
|
|
631
|
+
[0.11.13]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.12...v0.11.13
|
|
632
|
+
[0.11.12]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.11...v0.11.12
|
|
600
633
|
[0.11.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.10...v0.11.11
|
|
601
634
|
[0.11.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.9...v0.11.10
|
|
602
635
|
[0.11.9]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.8...v0.11.9
|
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.11.
|
|
6
|
+
**Package:** `sproutboat` 0.11.13 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -76,7 +76,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
76
76
|
| | |
|
|
77
77
|
| --- | --- |
|
|
78
78
|
| Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
|
|
79
|
-
| Provenance stamp | `zig-musl/0.16.0+porffor/
|
|
79
|
+
| Provenance stamp | `zig-musl/0.16.0+porffor/de4eb58+uws/360c276d` |
|
|
80
80
|
| Artifact schema | `2` |
|
|
81
81
|
| Runtime | `native-fetch` |
|
|
82
82
|
| Capability profile | `http-sync-v0` |
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -6,10 +6,9 @@ to its upstream source.
|
|
|
6
6
|
|
|
7
7
|
| Component | Version or revision | Distribution role | License | Source |
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
|
-
| Porffor | `
|
|
9
|
+
| Porffor | `de4eb588264885b3a1596f75010e371a2052033f` | Acquired on first application build | MIT | https://github.com/CanadaHonk/porffor |
|
|
10
10
|
| Zig | `0.16.0` | Acquired cross-compiler | MIT | https://ziglang.org |
|
|
11
11
|
| uWebSockets and uSockets | `360c276d609d59af56ae6932adb95154ace9f15f` | Embedded source and static archive | Apache-2.0 | https://github.com/uNetworking/uWebSockets |
|
|
12
|
-
| esbuild | `0.28.2` | Embedded platform executable | MIT | https://esbuild.github.io |
|
|
13
12
|
| SQLite | `3.50.4` | Acquired for standalone builds | Public domain | https://sqlite.org |
|
|
14
13
|
| BearSSL | `0.6` | Acquired for standalone builds | MIT | https://bearssl.org |
|
|
15
14
|
|
|
@@ -17,7 +16,7 @@ to its upstream source.
|
|
|
17
16
|
|
|
18
17
|
| Component | SHA-256 |
|
|
19
18
|
| --- | --- |
|
|
20
|
-
| Porffor source archive | `
|
|
19
|
+
| Porffor source archive | `1a62187a93356b36cb6ac703ce9770c8917e4fa65545fd7c33ee8a180ae5a4c9` |
|
|
21
20
|
| Embedded uWebSockets archive | `e83736f3f8cf9d56a1ebe6ea61625a7af12386763374d47c14cff472ada7484a` |
|
|
22
21
|
| SQLite amalgamation archive | `1d3049dd0f830a025a53105fc79fd2ab9431aea99e137809d064d8ee8356b032` |
|
|
23
22
|
| BearSSL source archive | `6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139efc83ff14` |
|
|
@@ -25,5 +24,4 @@ to its upstream source.
|
|
|
25
24
|
|
|
26
25
|
The complete Apache-2.0 notice for uWebSockets and uSockets is retained in the
|
|
27
26
|
embedded source archive. Porffor and BearSSL retain their MIT notices in the
|
|
28
|
-
source archives acquired at the revisions above.
|
|
29
|
-
available from its upstream distribution.
|
|
27
|
+
source archives acquired at the revisions above.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.13",
|
|
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",
|
|
@@ -61,13 +61,12 @@
|
|
|
61
61
|
"kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@sproutboat/artifact": "^0.
|
|
64
|
+
"@sproutboat/artifact": "^0.3.0",
|
|
65
65
|
"@sproutboat/assets": "^0.3.0",
|
|
66
66
|
"@sproutboat/config": "^0.4.0",
|
|
67
|
-
"@sproutboat/runtime": "^0.12.
|
|
68
|
-
"@sproutboat/toolchain": "^0.4.
|
|
69
|
-
"@sproutboat/wire": "^0.9.
|
|
70
|
-
"esbuild": "^0.28.2"
|
|
67
|
+
"@sproutboat/runtime": "^0.12.1",
|
|
68
|
+
"@sproutboat/toolchain": "^0.4.13",
|
|
69
|
+
"@sproutboat/wire": "^0.9.1"
|
|
71
70
|
},
|
|
72
71
|
"devDependencies": {
|
|
73
72
|
"@oxlint/plugins": "1.81.0",
|
|
@@ -78,10 +77,10 @@
|
|
|
78
77
|
"typescript": "7.0.2"
|
|
79
78
|
},
|
|
80
79
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.11.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.11.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.11.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "0.11.
|
|
80
|
+
"@sproutboat/cli-darwin-arm64": "0.11.13",
|
|
81
|
+
"@sproutboat/cli-darwin-x64": "0.11.13",
|
|
82
|
+
"@sproutboat/cli-linux-arm64": "0.11.13",
|
|
83
|
+
"@sproutboat/cli-linux-x64": "0.11.13"
|
|
85
84
|
},
|
|
86
85
|
"engines": {
|
|
87
86
|
"node": ">=18"
|
package/scripts/build-cli.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* GitHub's only x64 macOS image (`macos-13`) routinely sits queued for half an
|
|
16
16
|
* hour while every other leg finishes in one minute.
|
|
17
17
|
*/
|
|
18
|
-
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
|
18
|
+
import { chmod, copyFile, mkdir, rm } from "node:fs/promises";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { ensureZigArchive, type ZigPlatform } from "../src/toolchain";
|
|
21
21
|
|
|
@@ -39,49 +39,11 @@ function hostTarget(): Target {
|
|
|
39
39
|
|
|
40
40
|
const root = resolve(import.meta.dir, "..");
|
|
41
41
|
|
|
42
|
-
/** esbuild ships one binary per platform (`@esbuild/<os>-<arch>`); a cross
|
|
43
|
-
* target's copy is never installed locally (npm/bun refuse it — the package
|
|
44
|
-
* declares `os`/`cpu` and gets skipped for any other host), so fetch that
|
|
45
|
-
* platform's tarball from the registry instead, same shape as ensureZig's
|
|
46
|
-
* prebuilt-archive fetches. */
|
|
47
|
-
async function esbuildBinaryFor(target: Target): Promise<string> {
|
|
48
|
-
const host = hostTarget();
|
|
49
|
-
if (target.platform === host.platform && target.arch === host.arch) {
|
|
50
|
-
try {
|
|
51
|
-
return Bun.resolveSync(`@esbuild/${target.platform}-${target.arch}/bin/esbuild`, import.meta.dir);
|
|
52
|
-
} catch {
|
|
53
|
-
throw new Error("host esbuild binary is missing; run bun install first");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// SAFETY: package.json is this repo's own manifest, with a required esbuild dependency entry.
|
|
57
|
-
const pkg = (await Bun.file(resolve(root, "package.json")).json()) as { dependencies: { esbuild: string } };
|
|
58
|
-
const version = pkg.dependencies.esbuild.replace(/^[\^~]/, "");
|
|
59
|
-
const name = `${target.platform}-${target.arch}`;
|
|
60
|
-
const cacheDir = resolve(
|
|
61
|
-
process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? `${process.env.HOME}/.cache/sproutboat`,
|
|
62
|
-
`esbuild-${version}-${name}`,
|
|
63
|
-
);
|
|
64
|
-
const cached = resolve(cacheDir, "esbuild");
|
|
65
|
-
if (await Bun.file(cached).exists()) return cached;
|
|
66
|
-
const res = await fetch(`https://registry.npmjs.org/@esbuild/${name}/-/${name}-${version}.tgz`);
|
|
67
|
-
if (!res.ok) throw new Error(`could not fetch @esbuild/${name}@${version}: HTTP ${res.status}`);
|
|
68
|
-
const tarPath = resolve(cacheDir, "esbuild.tgz");
|
|
69
|
-
await mkdir(cacheDir, { recursive: true });
|
|
70
|
-
await Bun.write(tarPath, await res.arrayBuffer());
|
|
71
|
-
const untar = Bun.spawn(["tar", "-xzf", tarPath, "-C", cacheDir, "--strip-components=2", "package/bin/esbuild"], {
|
|
72
|
-
stdout: "pipe",
|
|
73
|
-
stderr: "pipe",
|
|
74
|
-
});
|
|
75
|
-
const [code, err] = await Promise.all([untar.exited, new Response(untar.stderr).text()]);
|
|
76
|
-
if (code !== 0) throw new Error(`could not extract @esbuild/${name}: ${err.trim()}`);
|
|
77
|
-
await chmod(cached, 0o755);
|
|
78
|
-
return cached;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
42
|
async function buildOne(target: Target): Promise<void> {
|
|
82
43
|
const packageDir = resolve(root, "platform-packages", `${target.platform}-${target.arch}`);
|
|
83
44
|
const out = resolve(packageDir, "bin", "sproutboat");
|
|
84
45
|
await mkdir(resolve(out, ".."), { recursive: true });
|
|
46
|
+
await rm(resolve(packageDir, "bin", "esbuild"), { force: true });
|
|
85
47
|
// SAFETY: package.json is this release's manifest and npm requires its version.
|
|
86
48
|
const version = ((await Bun.file(resolve(root, "package.json")).json()) as { version: string }).version;
|
|
87
49
|
// SAFETY: every checked-in platform package manifest owns a required string version.
|
|
@@ -103,9 +65,6 @@ async function buildOne(target: Target): Promise<void> {
|
|
|
103
65
|
{ cwd: root, stdout: "inherit", stderr: "inherit" },
|
|
104
66
|
);
|
|
105
67
|
if ((await child.exited) !== 0) throw new Error(`compile failed for ${target.platform}-${target.arch}`);
|
|
106
|
-
const esbuild = await esbuildBinaryFor(target);
|
|
107
|
-
const packagedEsbuild = resolve(packageDir, "bin", "esbuild");
|
|
108
|
-
await copyFile(esbuild, packagedEsbuild);
|
|
109
68
|
// The compressed archive (~50 MB), not the ~400 MB extracted install: Zig's
|
|
110
69
|
// lib/ carries libc/libc++ sources for every target it can cross-compile to,
|
|
111
70
|
// which only matters once, on extract. `ensureZig` (src/toolchain.ts) looks
|
|
@@ -114,7 +73,6 @@ async function buildOne(target: Target): Promise<void> {
|
|
|
114
73
|
// same shape as the vendored uWebSockets archive.
|
|
115
74
|
await copyFile(await ensureZigArchive(target.zigPlatform), resolve(packageDir, "bin", "zig.tar.xz"));
|
|
116
75
|
await copyFile(resolve(root, "THIRD_PARTY_NOTICES.md"), resolve(packageDir, "THIRD_PARTY_NOTICES.md"));
|
|
117
|
-
await chmod(packagedEsbuild, 0o755);
|
|
118
76
|
await chmod(out, 0o755);
|
|
119
77
|
}
|
|
120
78
|
|
package/src/bearssl.ts
CHANGED
|
@@ -34,7 +34,8 @@ const BEARSSL_SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139e
|
|
|
34
34
|
* release is unknown until the next one. Fine while releases are frequent;
|
|
35
35
|
* revisit if a user is ever stuck on an old CLI for months.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
// Use the dated archive so its bytes stay equal to the pinned digest.
|
|
38
|
+
const CACERT_URL = "https://curl.se/ca/cacert-2026-08-13.pem";
|
|
38
39
|
const CACERT_SHA256 = "f66dff1bdf8f96060b8177976f8b7d9254bc89bc4db933d769f7384d28480bc9";
|
|
39
40
|
|
|
40
41
|
const cacheDir = (): string =>
|
package/src/build.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, 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
7
|
import { sqliteStamp } from "./sqlite";
|
|
8
8
|
import { bearsslStamp, ensureBearssl } from "./bearssl";
|
|
9
|
-
import { compileSprout, type Transport } from "./compile";
|
|
9
|
+
import { compileSprout, loadPrelude, type Transport } from "./compile";
|
|
10
|
+
import { compileCached } from "./compile-cache";
|
|
10
11
|
import {
|
|
11
12
|
ARTIFACT_SCHEMA_VERSION,
|
|
12
13
|
CAPABILITY_PROFILE,
|
|
@@ -16,6 +17,10 @@ import {
|
|
|
16
17
|
type ArtifactManifest,
|
|
17
18
|
} from "./manifest";
|
|
18
19
|
import { ensureZig, esbuildVersion, porfforVersion, toolchainStamp } from "./toolchain";
|
|
20
|
+
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_COMMIT_FULL } from "./porffor-toolchain";
|
|
21
|
+
import { version as toolchainPackageVersion } from "@sproutboat/toolchain/package.json" with { type: "json" };
|
|
22
|
+
import { version as cliVersion } from "../package.json" with { type: "json" };
|
|
23
|
+
import { wrapNativeFetchHandler } from "./wrap";
|
|
19
24
|
|
|
20
25
|
export type BuildInput = {
|
|
21
26
|
projectDir: string;
|
|
@@ -47,11 +52,15 @@ export type BuildInput = {
|
|
|
47
52
|
reuseSproutPath?: string;
|
|
48
53
|
/** An isolated artifact directory, used by dev candidates to avoid mixing snapshots. */
|
|
49
54
|
outputDirectory?: string;
|
|
55
|
+
/** Stable compiler input used only by the local dev loop. */
|
|
56
|
+
generatedPath?: string;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
export type BuildOutput = {
|
|
53
60
|
artifactDir: string;
|
|
54
61
|
manifest: ArtifactManifest;
|
|
62
|
+
compileCache: "hit" | "miss" | "bypass";
|
|
63
|
+
compileMs: number;
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
/** Baking megabytes of assets into the module makes the Porffor compile crawl;
|
|
@@ -62,6 +71,39 @@ function digest(value: Uint8Array | string): `sha256:${string}` {
|
|
|
62
71
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
async function integrationIdentity(): Promise<string> {
|
|
75
|
+
// A published CLI has immutable bundled integration code and a release
|
|
76
|
+
// version. Source checkouts also hash their live files, so local edits never
|
|
77
|
+
// accidentally reuse a binary compiled by an earlier checkout state.
|
|
78
|
+
if (import.meta.url.includes("/$bunfs/")) {
|
|
79
|
+
const executable = await stat(process.execPath);
|
|
80
|
+
return `release:${cliVersion}:${executable.size}:${executable.mtimeMs}`;
|
|
81
|
+
}
|
|
82
|
+
const files = ["build.ts", "compile.ts", "sqlite.ts", "bearssl.ts", "toolchain.ts"].map((file) =>
|
|
83
|
+
resolve(import.meta.dir, file),
|
|
84
|
+
);
|
|
85
|
+
files.push(Bun.resolveSync("@sproutboat/toolchain/patch", import.meta.dir));
|
|
86
|
+
return digest(Buffer.concat(await Promise.all(files.map((file) => readFile(file)))));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function acquireArtifactLock(artifactDir: string): Promise<string> {
|
|
90
|
+
const lock = `${artifactDir}.lock`;
|
|
91
|
+
await mkdir(dirname(lock), { recursive: true });
|
|
92
|
+
const deadline = Date.now() + 12 * 60_000;
|
|
93
|
+
while (true) {
|
|
94
|
+
try {
|
|
95
|
+
await mkdir(lock);
|
|
96
|
+
return lock;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
99
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for artifact lock ${lock}`);
|
|
100
|
+
const info = await stat(lock).catch(() => null);
|
|
101
|
+
if (info && Date.now() - info.mtimeMs > 15 * 60_000) await rm(lock, { recursive: true, force: true });
|
|
102
|
+
else await Bun.sleep(100);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
/**
|
|
66
108
|
* Cross-compile a handler into a linux-x86_64 native-fetch server binary with
|
|
67
109
|
* Porffor + Zig — no Docker. The binary is not run here (the build host may not
|
|
@@ -73,30 +115,6 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
73
115
|
const sourceHash = digest(source);
|
|
74
116
|
const target = input.target ?? "linux-x86_64";
|
|
75
117
|
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);
|
|
97
|
-
const artifactDir = input.outputDirectory ?? resolve(input.projectDir, ".sproutboat/dist", artifactId);
|
|
98
|
-
const sproutPath = resolve(artifactDir, "sprout");
|
|
99
|
-
await mkdir(artifactDir, { recursive: true });
|
|
100
118
|
|
|
101
119
|
// #74 — split each storage-binding array into its binding-name list (the
|
|
102
120
|
// legacy shape the prelude/broker read) plus a `resources` map { binding ->
|
|
@@ -132,33 +150,34 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
132
150
|
resources,
|
|
133
151
|
};
|
|
134
152
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
153
|
+
const host = target === "host";
|
|
154
|
+
const assetDir = input.config.assets ? resolve(input.projectDir, input.config.assets.directory) : undefined;
|
|
155
|
+
if (
|
|
156
|
+
assetDir &&
|
|
157
|
+
!(await stat(assetDir)
|
|
158
|
+
.then((s) => s.isDirectory())
|
|
159
|
+
.catch(() => false))
|
|
160
|
+
) {
|
|
161
|
+
throw new Error(`assets.directory "${input.config.assets?.directory}" not found — run your site build first`);
|
|
162
|
+
}
|
|
163
|
+
const assetManifest: AssetManifest | undefined =
|
|
164
|
+
assetDir && input.config.assets
|
|
165
|
+
? {
|
|
166
|
+
notFound: input.config.assets.not_found_handling ?? "none",
|
|
167
|
+
runSproutFirst: input.config.assets.run_sprout_first ?? false,
|
|
168
|
+
files: walkAssets(assetDir),
|
|
169
|
+
}
|
|
170
|
+
: undefined;
|
|
146
171
|
// #15 — an embedded binary has no files beside it, so assets are baked into
|
|
147
172
|
// the module. Read them from the source directory: the artifact copy happens
|
|
148
173
|
// after the compile, and the compile is what needs them. Bytes travel as a
|
|
149
174
|
// latin1 string, one char per byte, which is what the asset shim hands back.
|
|
150
175
|
let bakedAssets: { manifest: AssetManifest; files: Record<string, string> } | undefined;
|
|
151
|
-
if (
|
|
152
|
-
const dir = resolve(input.projectDir, input.config.assets.directory);
|
|
153
|
-
const manifest: AssetManifest = {
|
|
154
|
-
notFound: input.config.assets.not_found_handling ?? "none",
|
|
155
|
-
runSproutFirst: input.config.assets.run_sprout_first ?? false,
|
|
156
|
-
files: walkAssets(dir),
|
|
157
|
-
};
|
|
176
|
+
if (embedded && assetDir && assetManifest) {
|
|
158
177
|
const files: Record<string, string> = {};
|
|
159
178
|
let total = 0;
|
|
160
|
-
for (const key of Object.keys(
|
|
161
|
-
const bytes = await readFile(resolve(
|
|
179
|
+
for (const key of Object.keys(assetManifest.files)) {
|
|
180
|
+
const bytes = await readFile(resolve(assetDir, `.${key}`));
|
|
162
181
|
total += bytes.byteLength;
|
|
163
182
|
if (total > MAX_BAKED_ASSET_BYTES) {
|
|
164
183
|
throw new Error(
|
|
@@ -168,85 +187,156 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
168
187
|
}
|
|
169
188
|
files[key] = bytes.toString("latin1");
|
|
170
189
|
}
|
|
171
|
-
bakedAssets = { manifest, files };
|
|
190
|
+
bakedAssets = { manifest: assetManifest, files };
|
|
172
191
|
}
|
|
173
192
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
193
|
+
// Version metadata is compiled into the executable. Its timestamp makes a
|
|
194
|
+
// new binary necessary on every build when this binding is configured.
|
|
176
195
|
const builtAt = new Date().toISOString();
|
|
196
|
+
const versionId = digest(
|
|
197
|
+
JSON.stringify({ sourceHash, target, config: input.config, assets: assetManifest, toolchain: toolchainStamp() }),
|
|
198
|
+
).slice("sha256:".length, 24);
|
|
177
199
|
const versionMetadata = input.config.version_metadata
|
|
178
|
-
? { binding: input.config.version_metadata, id:
|
|
200
|
+
? { binding: input.config.version_metadata, id: versionId, tag: input.config.name, timestamp: builtAt }
|
|
179
201
|
: undefined;
|
|
202
|
+
const generatedSource = wrapNativeFetchHandler(
|
|
203
|
+
source.toString(),
|
|
204
|
+
await loadPrelude(input.transport ?? "broker"),
|
|
205
|
+
input.config.vars ?? {},
|
|
206
|
+
bindings,
|
|
207
|
+
undefined,
|
|
208
|
+
input.config.compatibility_date,
|
|
209
|
+
input.config.name,
|
|
210
|
+
bakedAssets,
|
|
211
|
+
input.transport,
|
|
212
|
+
versionMetadata,
|
|
213
|
+
);
|
|
214
|
+
const compileKey = digest(
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
generatedSource,
|
|
217
|
+
target,
|
|
218
|
+
optimize: input.optimize ?? "release",
|
|
219
|
+
toolchain: toolchainStamp(),
|
|
220
|
+
toolchainPackageVersion,
|
|
221
|
+
porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
|
|
222
|
+
native: embedded ? [sqliteStamp(), bearsslStamp()] : [],
|
|
223
|
+
integration: await integrationIdentity(),
|
|
224
|
+
}),
|
|
225
|
+
).slice("sha256:".length);
|
|
226
|
+
const artifactId = versionMetadata
|
|
227
|
+
? versionId
|
|
228
|
+
: digest(JSON.stringify({ compileKey, config: input.config, assets: assetManifest })).slice("sha256:".length, 24);
|
|
229
|
+
const artifactDir = input.outputDirectory ?? resolve(input.projectDir, ".sproutboat/dist", artifactId);
|
|
230
|
+
const sproutPath = resolve(artifactDir, "sprout");
|
|
231
|
+
const artifactLock = await acquireArtifactLock(artifactDir);
|
|
232
|
+
try {
|
|
233
|
+
await mkdir(artifactDir, { recursive: true });
|
|
180
234
|
|
|
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
|
-
esbuildVersion: esbuildVersion(),
|
|
210
|
-
buildImage: toolchainStamp(),
|
|
211
|
-
compatibilityDate: input.config.compatibility_date,
|
|
212
|
-
sourceHash,
|
|
213
|
-
binaryHash: digest(sprout),
|
|
214
|
-
binarySize: (await stat(sproutPath)).size,
|
|
215
|
-
builtAt,
|
|
216
|
-
};
|
|
217
|
-
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
218
|
-
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
|
219
|
-
// frozen at v2. The control plane reads this to configure the per-deployment
|
|
220
|
-
// broker (KV / D1 / R2 / queue names, secret names, outbound allowlist, cron
|
|
221
|
-
// schedules, Durable Object classes).
|
|
222
|
-
const hasBindings =
|
|
223
|
-
Object.values(bindings).some((value) => Array.isArray(value) && value.length > 0) ||
|
|
224
|
-
Object.keys(bindings.resources).length > 0 ||
|
|
225
|
-
Object.keys(bindings.vars).length > 0;
|
|
226
|
-
if (hasBindings) {
|
|
227
|
-
await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Static assets: copy the directory next to the artifact and record a manifest
|
|
231
|
-
// the edge serves from directly (assets-first) and the broker reads for
|
|
232
|
-
// `env.<ASSETS>.fetch()`.
|
|
233
|
-
if (input.config.assets) {
|
|
234
|
-
const srcDir = resolve(input.projectDir, input.config.assets.directory);
|
|
235
|
-
const outDir = resolve(artifactDir, "assets");
|
|
235
|
+
const compile = async (outPath: string) => {
|
|
236
|
+
// Acquire native inputs only after a cache miss. A host build never needs
|
|
237
|
+
// the cross-compiler; embedded builds link SQLite and BearSSL as well.
|
|
238
|
+
const zigBin = host ? undefined : await ensureZig();
|
|
239
|
+
const [sqliteObject, tls] = embedded
|
|
240
|
+
? await Promise.all([ensureSqliteObject({ target, zigBin }), ensureBearssl({ target, zigBin })])
|
|
241
|
+
: [null, null];
|
|
242
|
+
await compileSprout({
|
|
243
|
+
sourcePath: input.sourcePath,
|
|
244
|
+
generatedPath: input.generatedPath,
|
|
245
|
+
generatedSource,
|
|
246
|
+
outPath,
|
|
247
|
+
vars: input.config.vars ?? {},
|
|
248
|
+
bindings,
|
|
249
|
+
zigBin,
|
|
250
|
+
target: input.target,
|
|
251
|
+
compatibilityDate: input.config.compatibility_date,
|
|
252
|
+
transport: input.transport,
|
|
253
|
+
appName: input.config.name,
|
|
254
|
+
assets: bakedAssets,
|
|
255
|
+
extraLink: [...(sqliteObject ? [sqliteObject] : []), ...(tls ? tls.objects : [])],
|
|
256
|
+
extraCflags: tls ? ["-I", tls.includeDir] : [],
|
|
257
|
+
optimize: input.optimize,
|
|
258
|
+
versionMetadata,
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
let compileCache: BuildOutput["compileCache"] = "bypass";
|
|
262
|
+
const compileStartedAt = performance.now();
|
|
236
263
|
if (
|
|
237
|
-
!
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
!input.reuseSproutPath &&
|
|
265
|
+
!host &&
|
|
266
|
+
input.optimize !== "dev" &&
|
|
267
|
+
!input.outputDirectory &&
|
|
268
|
+
!versionMetadata &&
|
|
269
|
+
!process.env.SPROUTBOAT_PORFFOR_DIR &&
|
|
270
|
+
!process.env.SPROUTBOAT_ZIG &&
|
|
271
|
+
!process.env.SPROUTBOAT_UWS_TARBALL &&
|
|
272
|
+
!process.env.SPROUTBOAT_BUILD_UWS_FROM_SOURCE &&
|
|
273
|
+
!process.env.PORFFOR_VERSION
|
|
240
274
|
) {
|
|
241
|
-
|
|
275
|
+
compileCache = await compileCached(
|
|
276
|
+
resolve(input.projectDir, ".sproutboat/compile-cache"),
|
|
277
|
+
compileKey,
|
|
278
|
+
sproutPath,
|
|
279
|
+
compile,
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
const candidate = resolve(artifactDir, `.sprout-${randomUUID()}`);
|
|
283
|
+
try {
|
|
284
|
+
if (input.reuseSproutPath) await cp(input.reuseSproutPath, candidate);
|
|
285
|
+
else await compile(candidate);
|
|
286
|
+
await rename(candidate, sproutPath);
|
|
287
|
+
} finally {
|
|
288
|
+
await rm(candidate, { force: true });
|
|
289
|
+
}
|
|
242
290
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
291
|
+
const compileMs = Math.round(performance.now() - compileStartedAt);
|
|
292
|
+
|
|
293
|
+
const sprout = await readFile(sproutPath);
|
|
294
|
+
const manifest: ArtifactManifest & { compileMs: number } = {
|
|
295
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
296
|
+
project: input.config.name,
|
|
297
|
+
target: host ? hostTarget() : DEPLOY_TARGET,
|
|
298
|
+
runtime: RUNTIME,
|
|
299
|
+
capabilityProfile: CAPABILITY_PROFILE,
|
|
300
|
+
porfforVersion: porfforVersion(),
|
|
301
|
+
esbuildVersion: esbuildVersion(),
|
|
302
|
+
buildImage: toolchainStamp(),
|
|
303
|
+
compatibilityDate: input.config.compatibility_date,
|
|
304
|
+
sourceHash,
|
|
305
|
+
binaryHash: digest(sprout),
|
|
306
|
+
binarySize: (await stat(sproutPath)).size,
|
|
307
|
+
compileMs,
|
|
308
|
+
builtAt,
|
|
248
309
|
};
|
|
249
|
-
await writeFile(resolve(artifactDir, "
|
|
310
|
+
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
311
|
+
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
|
312
|
+
// frozen at v2. The control plane reads this to configure the per-deployment
|
|
313
|
+
// broker (KV / D1 / R2 / queue names, secret names, outbound allowlist, cron
|
|
314
|
+
// schedules, Durable Object classes).
|
|
315
|
+
const hasBindings =
|
|
316
|
+
Object.values(bindings).some((value) => Array.isArray(value) && value.length > 0) ||
|
|
317
|
+
Object.keys(bindings.resources).length > 0 ||
|
|
318
|
+
Object.keys(bindings.vars).length > 0;
|
|
319
|
+
if (hasBindings) {
|
|
320
|
+
await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
|
|
321
|
+
} else await rm(resolve(artifactDir, "bindings.json"), { force: true });
|
|
322
|
+
|
|
323
|
+
// Static assets: copy the directory next to the artifact and record a manifest
|
|
324
|
+
// the edge serves from directly (assets-first) and the broker reads for
|
|
325
|
+
// `env.<ASSETS>.fetch()`.
|
|
326
|
+
if (assetDir && assetManifest) {
|
|
327
|
+
const outDir = resolve(artifactDir, "assets");
|
|
328
|
+
await rm(outDir, { recursive: true, force: true });
|
|
329
|
+
await cp(assetDir, outDir, { recursive: true });
|
|
330
|
+
if (JSON.stringify(walkAssets(outDir)) !== JSON.stringify(assetManifest.files)) {
|
|
331
|
+
throw new Error("assets changed during the build; retry after the asset build finishes");
|
|
332
|
+
}
|
|
333
|
+
await writeFile(resolve(artifactDir, "assets.json"), `${JSON.stringify(assetManifest, null, 2)}\n`);
|
|
334
|
+
} else {
|
|
335
|
+
await rm(resolve(artifactDir, "assets.json"), { force: true });
|
|
336
|
+
await rm(resolve(artifactDir, "assets"), { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
return { artifactDir, manifest, compileCache, compileMs };
|
|
339
|
+
} finally {
|
|
340
|
+
await rm(artifactLock, { recursive: true, force: true });
|
|
250
341
|
}
|
|
251
|
-
return { artifactDir, manifest };
|
|
252
342
|
}
|