@sproutboat/toolchain 0.4.17 → 0.4.19
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 +12 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +12 -1
- package/src/patch.test.ts +3 -1
- package/src/patch.ts +69 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.19
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 872198d: Standalone binaries accept HTTP/1.0 requests instead of answering 505 (baronunread/sproutboat#236), so they work behind nginx's default `proxy_http_version 1.0`. uWebSockets only parsed `HTTP/1.1` request lines. A helper now edits its `HttpParser.h` before each native-fetch compile to accept `HTTP/1.0` and mark the request as ancient, which makes uWebSockets close the connection after the response, as HTTP/1.0 expects.
|
|
8
|
+
|
|
9
|
+
## 0.4.18
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e0d0e17: Compile every unit with `-ffp-contract=off` (baronunread/sproutboat#235). On arm64, clang fused `a * b + c` into a single FMA that rounds once, where JavaScript rounds after the multiply and again after the add, so native sprouts could compute different doubles from V8 for the same code. A seeded LCG diverged at step 23.
|
|
14
|
+
|
|
3
15
|
## 0.4.17
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -29,7 +29,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
29
29
|
" return -1;\n" +
|
|
30
30
|
"}\n" +
|
|
31
31
|
"#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
|
|
32
|
-
"compiler/index.js":
|
|
32
|
+
"compiler/index.js":
|
|
33
|
+
"import { hashId } from './modules.js';\n" +
|
|
34
|
+
" const uwsDir = uwebsockets.ensureUWebSockets();\n" +
|
|
35
|
+
" '-fno-ident', '-ffunction-sections', '-fdata-sections',\n ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
|
|
33
36
|
"compiler/precompile.js":
|
|
34
37
|
"const fs = require('node:fs');\n" +
|
|
35
38
|
"const path = require('node:path');\n" +
|
|
@@ -46,6 +49,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
46
49
|
" len = i;\n" +
|
|
47
50
|
" }\n\n arr.length = len;\n\n return new ${name}(arr);\n};\n",
|
|
48
51
|
"compiler/uwebsockets.js":
|
|
52
|
+
'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n' +
|
|
49
53
|
"static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
|
|
50
54
|
"int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
|
|
51
55
|
"static std::string_view lookup_status_line(i32 status) {\n" +
|
|
@@ -198,6 +202,13 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
198
202
|
},
|
|
199
203
|
});
|
|
200
204
|
expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
|
|
205
|
+
// #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
|
|
206
|
+
const index = await readFile(join(dir, "compiler/index.js"), "utf8");
|
|
207
|
+
expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
|
|
208
|
+
expect(index).toContain("const uwsDir = uwebsockets.ensureUWebSockets();\n sbPatchHttp10(uwsDir);");
|
|
209
|
+
expect(await readFile(join(dir, "compiler/sb-http10.js"), "utf8")).toContain("export const sbPatchHttp10");
|
|
210
|
+
// #235: every compiled unit keeps JavaScript's separate float roundings.
|
|
211
|
+
expect(await readFile(join(dir, "compiler/index.js"), "utf8")).toContain("'-ffp-contract=off',");
|
|
201
212
|
// #168: the real vendored source takes the null guard on the `then` probe.
|
|
202
213
|
expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
|
|
203
214
|
"if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
|
package/src/patch.test.ts
CHANGED
|
@@ -44,7 +44,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
|
|
|
44
44
|
// body limit (#56), the #156 status-line switch, #163's collect_headers, and
|
|
45
45
|
// #176's read_value forward decl / is_forbidden_response_header /
|
|
46
46
|
// write_response_value.
|
|
47
|
-
const SHIM = `
|
|
47
|
+
const SHIM = `export const makeUWebSocketsShimSource = () => \`
|
|
48
|
+
#include "App.h"
|
|
49
|
+
static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
|
|
48
50
|
|
|
49
51
|
int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
50
52
|
|
package/src/patch.ts
CHANGED
|
@@ -192,6 +192,69 @@ const CFLAGS_INJECT =
|
|
|
192
192
|
" ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
|
|
193
193
|
const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
|
|
194
194
|
|
|
195
|
+
// baronunread/sproutboat#235: clang's default -ffp-contract=on fuses `a * b + c`
|
|
196
|
+
// into one FMA on arm64, rounding once where JavaScript rounds twice. A seeded
|
|
197
|
+
// LCG (seed * 1103515245 + 12345, past 2^53) diverged from V8 at step 23.
|
|
198
|
+
// Every compiled unit keeps JS's separate roundings with contraction off.
|
|
199
|
+
const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n";
|
|
200
|
+
const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
|
|
201
|
+
const FP_CONTRACT_MARKER = "sproutboat #235";
|
|
202
|
+
|
|
203
|
+
// baronunread/sproutboat#236: uWebSockets answers any request line that isn't
|
|
204
|
+
// HTTP/1.1 with 505, so a standalone sprout behind nginx's default
|
|
205
|
+
// `proxy_http_version 1.0` failed every request. Its HttpRequest already has
|
|
206
|
+
// isAncient(), which closes the connection after the response; the parser just
|
|
207
|
+
// never sets it. uWebSockets is header-only and lives in Porffor's deps cache,
|
|
208
|
+
// not in the Porffor checkout, so a helper written next to compiler/index.js
|
|
209
|
+
// edits HttpParser.h right before each native-fetch compile (marker-guarded).
|
|
210
|
+
const HTTP10_HELPER = String.raw`// sproutboat #236: accept HTTP/1.0 request lines in uWebSockets.
|
|
211
|
+
import fs from 'node:fs';
|
|
212
|
+
|
|
213
|
+
const MARK = 'sb_http10_request';
|
|
214
|
+
const edits = [
|
|
215
|
+
[
|
|
216
|
+
'struct HttpParser {',
|
|
217
|
+
'/* sproutboat #236: set by consumeRequestLine for an HTTP/1.0 request line. */\ninline thread_local bool sb_http10_request = false;\n\nstruct HttpParser {'
|
|
218
|
+
],
|
|
219
|
+
[
|
|
220
|
+
'/* Scan until single SP, assume next is / (origin request) */\n char *start = data;',
|
|
221
|
+
'/* Scan until single SP, assume next is / (origin request) */\n sb_http10_request = false;\n char *start = data;'
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }',
|
|
225
|
+
'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }'
|
|
226
|
+
],
|
|
227
|
+
[
|
|
228
|
+
'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }',
|
|
229
|
+
'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, 11) == 0) {\n sb_http10_request = true;\n return data + 11;\n }'
|
|
230
|
+
],
|
|
231
|
+
[ 'req->ancientHttp = false;', 'req->ancientHttp = sb_http10_request;' ]
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
export const sbPatchHttp10 = uwsDir => {
|
|
235
|
+
const file = uwsDir + '/src/HttpParser.h';
|
|
236
|
+
let src = fs.readFileSync(file, 'utf8');
|
|
237
|
+
if (src.includes(MARK)) return;
|
|
238
|
+
for (const [ from, to ] of edits) {
|
|
239
|
+
if (!src.includes(from)) throw new Error('sproutboat #236: uWebSockets HttpParser.h changed, anchor not found: ' + from.slice(0, 60));
|
|
240
|
+
src = src.replace(from, to);
|
|
241
|
+
}
|
|
242
|
+
fs.writeFileSync(file, src);
|
|
243
|
+
};
|
|
244
|
+
`;
|
|
245
|
+
const HTTP10_IMPORT_ANCHOR = "import { hashId } from './modules.js';\n";
|
|
246
|
+
const HTTP10_IMPORT_INJECT = "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
|
|
247
|
+
const HTTP10_IMPORT_MARKER = "./sb-http10.js";
|
|
248
|
+
const HTTP10_CALL_ANCHOR = " const uwsDir = uwebsockets.ensureUWebSockets();\n";
|
|
249
|
+
const HTTP10_CALL_INJECT = " sbPatchHttp10(uwsDir);\n";
|
|
250
|
+
const HTTP10_CALL_MARKER = "sbPatchHttp10(uwsDir)";
|
|
251
|
+
// The shim is content-cached, so a header edit alone would never recompile it.
|
|
252
|
+
// This line changes the shim once, which rebuilds it against the edited header.
|
|
253
|
+
const HTTP10_SHIM_ANCHOR = 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
|
|
254
|
+
const HTTP10_SHIM_INJECT =
|
|
255
|
+
'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n// sproutboat #236: built against an HttpParser.h that accepts HTTP/1.0\n';
|
|
256
|
+
const HTTP10_SHIM_MARKER = "sproutboat #236: built against";
|
|
257
|
+
|
|
195
258
|
// baronunread/sproutboat#168: Porffor represents null as an object-typed value
|
|
196
259
|
// with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
|
|
197
260
|
// promise resolving to null as an object and looked `then` up at memory offset
|
|
@@ -767,6 +830,7 @@ const done = new Set<string>();
|
|
|
767
830
|
|
|
768
831
|
/** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
|
|
769
832
|
const UWS_EDITS = [
|
|
833
|
+
[HTTP10_SHIM_MARKER, HTTP10_SHIM_ANCHOR, HTTP10_SHIM_INJECT, "shim rebuild for HTTP/1.0 (#236)"],
|
|
770
834
|
[BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
|
|
771
835
|
[STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
|
|
772
836
|
[STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
|
|
@@ -953,12 +1017,16 @@ export async function patchUwebsockets(root: string): Promise<void> {
|
|
|
953
1017
|
}
|
|
954
1018
|
|
|
955
1019
|
async function patchCompilerArgs(root: string): Promise<void> {
|
|
1020
|
+
await writeFile(resolve(root, "compiler/sb-http10.js"), HTTP10_HELPER);
|
|
956
1021
|
const file = resolve(root, "compiler/index.js");
|
|
957
1022
|
let src = await readFile(file, "utf8");
|
|
958
1023
|
let changed = false;
|
|
959
1024
|
for (const [marker, anchor, inject, what] of [
|
|
960
1025
|
[LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
|
|
961
1026
|
[CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
|
|
1027
|
+
[FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
|
|
1028
|
+
[HTTP10_IMPORT_MARKER, HTTP10_IMPORT_ANCHOR, HTTP10_IMPORT_INJECT, "HTTP/1.0 helper import (#236)"],
|
|
1029
|
+
[HTTP10_CALL_MARKER, HTTP10_CALL_ANCHOR, HTTP10_CALL_INJECT, "HTTP/1.0 parser edit (#236)"],
|
|
962
1030
|
] as const) {
|
|
963
1031
|
if (src.includes(marker)) continue;
|
|
964
1032
|
const at = src.indexOf(anchor);
|
|
@@ -971,7 +1039,7 @@ async function patchCompilerArgs(root: string): Promise<void> {
|
|
|
971
1039
|
// Add flags to compileOnlyArgs so every generated C unit sees them and
|
|
972
1040
|
// Porffor's module-build cache includes them in its compiler command.
|
|
973
1041
|
src =
|
|
974
|
-
marker
|
|
1042
|
+
marker !== LINK_MARKER
|
|
975
1043
|
? src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length)
|
|
976
1044
|
: src.slice(0, at) + inject + src.slice(at);
|
|
977
1045
|
changed = true;
|