@sproutboat/toolchain 0.4.10 → 0.4.11
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 +6 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +43 -0
- package/src/acquire.ts +16 -2
- package/src/patch.ts +54 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6d72af7: Fix `TypedArray.from` with array-like inputs in native binaries, and regenerate Porffor's builtins table when its compiler is unpacked through a symlinked temporary path. This preserves the key bytes in repeated HMAC signing with `Uint8Array.from({ length: 16 }, mapFn)`.
|
|
8
|
+
|
|
3
9
|
## 0.4.10
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -29,6 +29,20 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
29
29
|
" return -1;\n" +
|
|
30
30
|
"}\n",
|
|
31
31
|
"compiler/index.js": " '-xc', '-', '-c',\n uSocketsArchive,\n '-lm'\n",
|
|
32
|
+
"compiler/precompile.js":
|
|
33
|
+
"const fs = require('node:fs');\n" +
|
|
34
|
+
"const path = require('node:path');\n" +
|
|
35
|
+
"fs.writeFileSync(path.join(__dirname, 'builtins_precompiled.js'), fs.readFileSync(path.join(__dirname, 'builtins/typedarray.js')));\n",
|
|
36
|
+
"compiler/builtins_precompiled.js": "fixture:precompiled\n",
|
|
37
|
+
"compiler/builtins/typedarray.js":
|
|
38
|
+
"export const __${name}_from = (arg: any, mapFn: any): ${name} => {\n" +
|
|
39
|
+
" const arr: any[] = Porffor.array.new(4);\n" +
|
|
40
|
+
" let len: i32 = 0;\n" +
|
|
41
|
+
" if (Porffor.type(arg) == Porffor.TYPES.array) {\n" +
|
|
42
|
+
" let i: i32 = 0;\n" +
|
|
43
|
+
" for (const x of arg) arr[i++] = x;\n" +
|
|
44
|
+
" len = i;\n" +
|
|
45
|
+
" }\n\n arr.length = len;\n\n return new ${name}(arr);\n};\n",
|
|
32
46
|
"compiler/uwebsockets.js":
|
|
33
47
|
"static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
|
|
34
48
|
"int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
|
|
@@ -202,6 +216,35 @@ test("a corrupted warm cache is replaced from the verified archive", async () =>
|
|
|
202
216
|
expect(await readFile(join(root, "compiler/render.js"), "utf8")).toContain('getenv("PORT")');
|
|
203
217
|
});
|
|
204
218
|
|
|
219
|
+
test("a cache built by an older toolchain version is rebuilt, not silently reused", async () => {
|
|
220
|
+
const { archive, sha256 } = await fixture();
|
|
221
|
+
const cacheRoot = await mkdtemp(join(tmpdir(), "sb-porffor-stale-toolchain-"));
|
|
222
|
+
temporary.push(cacheRoot);
|
|
223
|
+
let requests = 0;
|
|
224
|
+
const options = {
|
|
225
|
+
cacheRoot,
|
|
226
|
+
url: "fixture",
|
|
227
|
+
expectedSha256: sha256,
|
|
228
|
+
fetcher: async () => {
|
|
229
|
+
requests += 1;
|
|
230
|
+
return new Response(Bun.file(archive));
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const root = await ensurePorffor(options);
|
|
234
|
+
const manifestPath = join(root, ".sproutboat-complete");
|
|
235
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
236
|
+
expect(manifest.toolchainVersion).toBeTruthy();
|
|
237
|
+
// Simulate a cache left behind by an older @sproutboat/toolchain publish
|
|
238
|
+
// (baronunread/sproutboat#205): a patch.ts fix landing in a new version used
|
|
239
|
+
// to sit unused because the cache key only tracked the upstream commit.
|
|
240
|
+
await rm(manifestPath);
|
|
241
|
+
await writeFile(manifestPath, JSON.stringify({ ...manifest, toolchainVersion: "0.0.0-stale" }));
|
|
242
|
+
await ensurePorffor(options);
|
|
243
|
+
expect(requests).toBe(2);
|
|
244
|
+
const rebuilt = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
245
|
+
expect(rebuilt.toolchainVersion).toBe(manifest.toolchainVersion);
|
|
246
|
+
});
|
|
247
|
+
|
|
205
248
|
test("an interrupted stale lock is recovered", async () => {
|
|
206
249
|
const { archive, sha256 } = await fixture();
|
|
207
250
|
const cacheRoot = await mkdtemp(join(tmpdir(), "sb-porffor-interrupted-"));
|
package/src/acquire.ts
CHANGED
|
@@ -8,6 +8,12 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import vendoredPorfforArchive from "../vendor/porffor-8f01541.tar.gz" with { type: "file" };
|
|
9
9
|
import { ensurePorfforPatched } from "./patch";
|
|
10
10
|
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_ARCHIVE_URL, PORFFOR_COMMIT_FULL } from "./pin";
|
|
11
|
+
// Folded into the cache manifest below so a toolchain release that changes
|
|
12
|
+
// patch.ts's output invalidates every machine's cache automatically
|
|
13
|
+
// (baronunread/sproutboat#205 — a patch fix published as a new version used
|
|
14
|
+
// to sit unused behind a still-"complete" cache keyed only on the upstream
|
|
15
|
+
// Porffor commit).
|
|
16
|
+
import { version as TOOLCHAIN_VERSION } from "../package.json" with { type: "json" };
|
|
11
17
|
|
|
12
18
|
export class PorfforToolchainError extends Error {
|
|
13
19
|
constructor(
|
|
@@ -31,6 +37,9 @@ const required = [
|
|
|
31
37
|
"compiler/render.js",
|
|
32
38
|
"compiler/index.js",
|
|
33
39
|
"compiler/uwebsockets.js",
|
|
40
|
+
"compiler/precompile.js",
|
|
41
|
+
"compiler/builtins/typedarray.js",
|
|
42
|
+
"compiler/builtins_precompiled.js",
|
|
34
43
|
];
|
|
35
44
|
async function digest(path: string): Promise<string> {
|
|
36
45
|
return createHash("sha256")
|
|
@@ -45,9 +54,14 @@ async function complete(dir: string, expectedArchive = PORFFOR_ARCHIVE_SHA256):
|
|
|
45
54
|
const manifest = JSON.parse(await readFile(resolve(dir, ".sproutboat-complete"), "utf8")) as {
|
|
46
55
|
commit?: string;
|
|
47
56
|
archiveSha256?: string;
|
|
57
|
+
toolchainVersion?: string;
|
|
48
58
|
files?: Record<string, string>;
|
|
49
59
|
};
|
|
50
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
manifest.commit !== PORFFOR_COMMIT_FULL ||
|
|
62
|
+
manifest.archiveSha256 !== expectedArchive ||
|
|
63
|
+
manifest.toolchainVersion !== TOOLCHAIN_VERSION
|
|
64
|
+
)
|
|
51
65
|
return false;
|
|
52
66
|
for (const file of required)
|
|
53
67
|
if (!manifest.files?.[file] || (await digest(resolve(dir, file))) !== manifest.files[file])
|
|
@@ -185,7 +199,7 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
|
|
|
185
199
|
);
|
|
186
200
|
await writeFile(
|
|
187
201
|
resolve(stage, ".sproutboat-complete"),
|
|
188
|
-
JSON.stringify({ commit: PORFFOR_COMMIT_FULL, archiveSha256: actual, files }),
|
|
202
|
+
JSON.stringify({ commit: PORFFOR_COMMIT_FULL, archiveSha256: actual, toolchainVersion: TOOLCHAIN_VERSION, files }),
|
|
189
203
|
{ mode: 0o444 },
|
|
190
204
|
);
|
|
191
205
|
await rename(stage, dir);
|
package/src/patch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Idempotent, marker-guarded in-place edits to Porffor's generated C
|
|
3
|
-
* (`compiler/render.js`, `compiler/index.js`, `compiler/uwebsockets.js
|
|
3
|
+
* (`compiler/render.js`, `compiler/index.js`, `compiler/uwebsockets.js`,
|
|
4
|
+
* `compiler/builtins/typedarray.js`). Run
|
|
4
5
|
* from the build path, not a `postinstall` hook: package managers block
|
|
5
6
|
* dependency lifecycle scripts by default, so a published `postinstall` would
|
|
6
7
|
* silently not run.
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
* older version of this module still receives the newer edits. All of them are
|
|
10
11
|
* tracked in patches/UPSTREAM.md and go away as Porffor closes the gaps.
|
|
11
12
|
*/
|
|
12
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { readFile, realpath, writeFile } from "node:fs/promises";
|
|
13
14
|
import { resolve } from "node:path";
|
|
14
15
|
|
|
15
16
|
// --- compiler/render.js: the native-fetch server, rendered as C text ---
|
|
@@ -167,6 +168,28 @@ const CFLAGS_INJECT =
|
|
|
167
168
|
" ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
|
|
168
169
|
const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
|
|
169
170
|
|
|
171
|
+
// Porffor's TypedArray.from only handles iterables. An array-like input such
|
|
172
|
+
// as { length: 16 } silently becomes an empty typed array, including HMAC
|
|
173
|
+
// keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
|
|
174
|
+
// precompiled builtin table, so the edited source must be precompiled again.
|
|
175
|
+
const TYPED_ARRAY_FROM_ANCHOR =
|
|
176
|
+
" len = i;\n }\n\n arr.length = len;\n\n return new ${name}(arr);\n};";
|
|
177
|
+
const TYPED_ARRAY_FROM_INJECT =
|
|
178
|
+
" len = i;\n" +
|
|
179
|
+
" } else {\n" +
|
|
180
|
+
" // sproutboat: TypedArray.from accepts array-like objects.\n" +
|
|
181
|
+
" let count = ecma262.ToIntegerOrInfinity(Porffor.object.get(arg, 'length'));\n" +
|
|
182
|
+
" if (count < 0) count = 0;\n" +
|
|
183
|
+
" if (count > 2147483643) throw new RangeError('Invalid TypedArray length (over maximum supported length)');\n" +
|
|
184
|
+
" if (Porffor.type(mapFn) != Porffor.TYPES.undefined && Porffor.type(mapFn) != Porffor.TYPES.function)\n" +
|
|
185
|
+
" throw new TypeError('Called TypedArray.from with a non-function mapFn');\n" +
|
|
186
|
+
" for (let i: i32 = 0; i < count; i++) {\n" +
|
|
187
|
+
" arr[i] = Porffor.type(mapFn) == Porffor.TYPES.undefined ? arg[i] : mapFn(arg[i], i);\n" +
|
|
188
|
+
" }\n" +
|
|
189
|
+
" len = count;\n" +
|
|
190
|
+
" }\n\n arr.length = len;\n\n return new ${name}(arr);\n};";
|
|
191
|
+
const TYPED_ARRAY_FROM_MARKER = "sproutboat: TypedArray.from accepts array-like objects";
|
|
192
|
+
|
|
170
193
|
/**
|
|
171
194
|
* #56 — make the inbound request-body limit configurable.
|
|
172
195
|
*
|
|
@@ -830,6 +853,34 @@ async function patchCompilerArgs(root: string): Promise<void> {
|
|
|
830
853
|
if (changed) await writeFile(file, src);
|
|
831
854
|
}
|
|
832
855
|
|
|
856
|
+
export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
857
|
+
const file = resolve(root, "compiler/builtins/typedarray.js");
|
|
858
|
+
const src = await readFile(file, "utf8");
|
|
859
|
+
if (!src.includes(TYPED_ARRAY_FROM_MARKER) && !src.includes(TYPED_ARRAY_FROM_ANCHOR)) {
|
|
860
|
+
throw new Error(`could not patch Porffor's TypedArray.from: anchor not found in ${file}`);
|
|
861
|
+
}
|
|
862
|
+
if (!src.includes(TYPED_ARRAY_FROM_MARKER))
|
|
863
|
+
await writeFile(file, src.replace(TYPED_ARRAY_FROM_ANCHOR, TYPED_ARRAY_FROM_INJECT));
|
|
864
|
+
const table = resolve(root, "compiler/builtins_precompiled.js");
|
|
865
|
+
const before = await readFile(table);
|
|
866
|
+
// Porffor only writes the table when import.meta.url matches argv[1].
|
|
867
|
+
// macOS's /var -> /private/var symlink makes a noncanonical argv[1] skip it.
|
|
868
|
+
const precompile = await realpath(resolve(root, "compiler/precompile.js"));
|
|
869
|
+
const child = Bun.spawn(["node", precompile], {
|
|
870
|
+
cwd: root,
|
|
871
|
+
stdout: "pipe",
|
|
872
|
+
stderr: "pipe",
|
|
873
|
+
});
|
|
874
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
875
|
+
child.exited,
|
|
876
|
+
new Response(child.stdout).text(),
|
|
877
|
+
new Response(child.stderr).text(),
|
|
878
|
+
]);
|
|
879
|
+
if (code !== 0) throw new Error(`could not precompile Porffor's TypedArray.from: ${stderr || stdout}`);
|
|
880
|
+
if (!src.includes(TYPED_ARRAY_FROM_MARKER) && before.equals(await readFile(table)))
|
|
881
|
+
throw new Error(`Porffor precompile did not update ${table}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
833
884
|
/** The native-fetch server source: $PORT support + the #165 console sink. Exported for tests. */
|
|
834
885
|
export async function patchRenderJs(root: string): Promise<void> {
|
|
835
886
|
const file = resolve(root, "compiler/render.js");
|
|
@@ -876,5 +927,6 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
876
927
|
await patchCompilerArgs(root);
|
|
877
928
|
await patchUwebsockets(root);
|
|
878
929
|
await patchRenderJs(root);
|
|
930
|
+
await patchTypedArrayFrom(root);
|
|
879
931
|
done.add(root);
|
|
880
932
|
}
|