@wasm-oj/server 0.2.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/LICENSE +21 -0
- package/README.md +6 -0
- package/THIRD_PARTY_NOTICES.md +302 -0
- package/crates/runtime-core/Cargo.lock +5099 -0
- package/crates/runtime-core/Cargo.toml +66 -0
- package/crates/runtime-core/README.md +47 -0
- package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
- package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
- package/crates/runtime-core/src/capabilities.rs +118 -0
- package/crates/runtime-core/src/compiler.rs +658 -0
- package/crates/runtime-core/src/contract.rs +5 -0
- package/crates/runtime-core/src/deterministic.rs +1051 -0
- package/crates/runtime-core/src/error.rs +58 -0
- package/crates/runtime-core/src/filesystem.rs +547 -0
- package/crates/runtime-core/src/filesystem_quota.rs +167 -0
- package/crates/runtime-core/src/go_compiler_session.rs +297 -0
- package/crates/runtime-core/src/interactive.rs +1019 -0
- package/crates/runtime-core/src/judge_package.rs +1539 -0
- package/crates/runtime-core/src/lib.rs +98 -0
- package/crates/runtime-core/src/memory.rs +84 -0
- package/crates/runtime-core/src/meter.rs +549 -0
- package/crates/runtime-core/src/module_imports.rs +149 -0
- package/crates/runtime-core/src/module_policy.rs +714 -0
- package/crates/runtime-core/src/output.rs +204 -0
- package/crates/runtime-core/src/run/mod.rs +208 -0
- package/crates/runtime-core/src/run/native.rs +260 -0
- package/crates/runtime-core/src/run/web.rs +229 -0
- package/crates/runtime-core/src/run/web_runtime.rs +109 -0
- package/crates/runtime-core/src/types.rs +268 -0
- package/crates/runtime-core/src/web.rs +83 -0
- package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
- package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
- package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
- package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
- package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
- package/dist/go-stage.mjs +193 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +4393 -0
- package/dist/java-stage.mjs +111 -0
- package/dist/python-stage.mjs +90 -0
- package/dist/rustc-stage.mjs +301 -0
- package/dist/server-build-stage.mjs +2564 -0
- package/dist/server-runner-stage.mjs +155 -0
- package/licenses/fflate-MIT.txt +21 -0
- package/licenses/runtime-core-dependencies.html +6253 -0
- package/licenses/runtime-core-dependencies.json +3041 -0
- package/licenses/wasmer-sdk-MIT.txt +21 -0
- package/licenses/wasmer-sdk-dependencies.html +6901 -0
- package/licenses/wasmer-sdk-dependencies.json +3013 -0
- package/package.json +70 -0
- package/rust-toolchain.toml +5 -0
- package/testdata/wojjdg02-v2-text.hex +1 -0
- package/vendor/shared-buffer/Cargo.toml +22 -0
- package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
- package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
- package/vendor/shared-buffer/README.md +34 -0
- package/vendor/shared-buffer/src/lib.rs +58 -0
- package/vendor/shared-buffer/src/mmap.rs +250 -0
- package/vendor/shared-buffer/src/owned.rs +389 -0
- package/vendor/virtual-fs/Cargo.toml +181 -0
- package/vendor/virtual-fs/LICENSE +25 -0
- package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
- package/vendor/virtual-fs/src/arc_file.rs +182 -0
- package/vendor/virtual-fs/src/arc_fs.rs +68 -0
- package/vendor/virtual-fs/src/buffer_file.rs +103 -0
- package/vendor/virtual-fs/src/builder.rs +232 -0
- package/vendor/virtual-fs/src/combine_file.rs +101 -0
- package/vendor/virtual-fs/src/cow_file.rs +345 -0
- package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
- package/vendor/virtual-fs/src/empty_fs.rs +81 -0
- package/vendor/virtual-fs/src/filesystems.rs +108 -0
- package/vendor/virtual-fs/src/host_fs.rs +1390 -0
- package/vendor/virtual-fs/src/lib.rs +782 -0
- package/vendor/virtual-fs/src/limiter.rs +252 -0
- package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
- package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
- package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
- package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
- package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
- package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
- package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
- package/vendor/virtual-fs/src/null_file.rs +87 -0
- package/vendor/virtual-fs/src/ops.rs +364 -0
- package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
- package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
- package/vendor/virtual-fs/src/pipe.rs +603 -0
- package/vendor/virtual-fs/src/random_file.rs +88 -0
- package/vendor/virtual-fs/src/special_file.rs +108 -0
- package/vendor/virtual-fs/src/static_file.rs +133 -0
- package/vendor/virtual-fs/src/static_fs.rs +460 -0
- package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
- package/vendor/virtual-fs/src/trace_fs.rs +258 -0
- package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
- package/vendor/virtual-fs/src/zero_file.rs +90 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4393 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { deserialize, serialize } from "node:v8";
|
|
8
|
+
import { gunzipSync } from "node:zlib";
|
|
9
|
+
import { Runtime, init } from "@wasmer/sdk/node";
|
|
10
|
+
import { WEIGHTED_METER_MODEL, asWasmOjError, assertCompilerCacheKey, assertValidBuildArtifact, assertValidProject, createDefaultDependencyManager, createDefaultRuntimeDrivers, createEngine, createExtendedCostBaselineRegistry, normalizeExecutionMetrics, prepareArtifactInteraction, prepareArtifactRun, prepareTrustedJudgeRun, toolchainAssetSource, toolchainCacheIdentity, toolchainProfileSource, unavailableExecutionMetrics, validateServerToolchainSources } from "@wasm-oj/core";
|
|
11
|
+
import { WASM_OJ_CONTRACT_VERSION, WASM_OJ_SCHEMAS, WASM_OJ_STORAGE, assertLanguageIdentifier, isBuiltinLanguage } from "@wasm-oj/contracts";
|
|
12
|
+
import { Directory, Wasmer } from "@wasmer/sdk";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
//#region src/core/toolchains.ts
|
|
15
|
+
var CLANG_VERSION = "22.0.0-git20542-10";
|
|
16
|
+
var CLANG_PACKAGE = `wasm-oj/clang@${CLANG_VERSION}`;
|
|
17
|
+
var CLANG_PACKAGE_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.webc.gz.bin`;
|
|
18
|
+
var CLANG_CC1_PINS_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.cc1-pins.json`;
|
|
19
|
+
var CLANG_MANIFEST_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.manifest.json`;
|
|
20
|
+
var CLANG_LIBCXX_PCH_MANIFEST_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.libcxx-pch.json`;
|
|
21
|
+
var CLANG_LIBCXX_PCH_DEBUG_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.cpp-debug.pch.gz.bin`;
|
|
22
|
+
var CLANG_LIBCXX_PCH_RELEASE_ASSET_PATH = `/toolchains/clang-${CLANG_VERSION}.cpp-release.pch.gz.bin`;
|
|
23
|
+
var CLANG_COMPRESSED_PACKAGE_SHA256 = "7f10d90b8e52b270f04874641a1d0bf9e94e85b4f6c7573a774cebbc6d32552a";
|
|
24
|
+
/** SHA-256 after browser-side gzip decompression. */
|
|
25
|
+
var CLANG_PACKAGE_SHA256 = "21ded33b9c6d4e1aaad5528c940bdaf6c3e84be77ea8f522f018ca7289a2a224";
|
|
26
|
+
/** SHA-256 of the pinned cc1/wasm-ld argv manifest; regenerated by scripts/pin-clang-cc1-argv.mjs. */
|
|
27
|
+
var CLANG_CC1_PINS_SHA256 = "66c4604dccd3f89d8e1472bf4432367d7396cce4a01279b1a1db445f229dba72";
|
|
28
|
+
var CLANG_MANIFEST_SHA256 = "6382dcdfb6a2da49032a0e08da3b1fb490eb24432be85c3c12e3e871a5065273";
|
|
29
|
+
/** Updated atomically with the generated toolchain-admitted PCH assets. */
|
|
30
|
+
var CLANG_LIBCXX_PCH_MANIFEST_SHA256 = "d126c99e951a7302d4ea2b66da4ed64d3d74e9d319d562518867c8d8c97a06b8";
|
|
31
|
+
var CLANG_LIBCXX_PCH = Object.freeze({
|
|
32
|
+
"cpp-debug": Object.freeze({
|
|
33
|
+
path: CLANG_LIBCXX_PCH_DEBUG_ASSET_PATH,
|
|
34
|
+
sha256: "05a4db721448ba33e65977cb4ac366908ea215ab377e3b6d09f1fd224d515b68",
|
|
35
|
+
compressedSha256: "a4152027d248412eca8aec3e7e23f6f7c81f95170cae9fd385bcf02e57e91fc9"
|
|
36
|
+
}),
|
|
37
|
+
"cpp-release": Object.freeze({
|
|
38
|
+
path: CLANG_LIBCXX_PCH_RELEASE_ASSET_PATH,
|
|
39
|
+
sha256: "aec9e778ac527c120ba369b9b0e142cf8206d11365801208847ba0e1c39daa1a",
|
|
40
|
+
compressedSha256: "18f4ca8ab8ca7888db572ba34146fc1acb213a7e7305000ea6285188f52f99f4"
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
var PYTHON_VERSION = "3.14.6";
|
|
44
|
+
var PYTHON_PACKAGE_SHA256 = "454ffc53936aa13a0d7f4afbb5bd50ada339c8ebd04bbf27ea19e4104cf43207";
|
|
45
|
+
var PYTHON_PACKAGE = `${`wasm-oj/cpython@${PYTHON_VERSION}`}#sha256:${PYTHON_PACKAGE_SHA256}`;
|
|
46
|
+
var PYTHON_PACKAGE_ASSET_PATH = `/toolchains/python-${PYTHON_VERSION}-wasip1.webc.gz.bin`;
|
|
47
|
+
var PYTHON_PACKAGE_MANIFEST_ASSET_PATH = `/toolchains/python-${PYTHON_VERSION}-wasip1.manifest.json`;
|
|
48
|
+
var PYTHON_COMPRESSED_PACKAGE_SHA256 = "218cd20ac4abb443e0700816010a615a345a43eae623a0232da2227135a6c7a6";
|
|
49
|
+
var PYTHON_PACKAGE_MANIFEST_SHA256 = "054eccad04a7cee7ba1661062142ef0d639976850981eab8fc785f48eb26129e";
|
|
50
|
+
var PYTHON_RUNTIME_FILES_ARCHIVE_SHA256 = "44d894f91487f20c2bb04fe496a9343db37d8720fb706472c2b4a7f3300db039";
|
|
51
|
+
var QUICKJS_VERSION = "0.15.1";
|
|
52
|
+
var QUICKJS_PACKAGE = `wasm-oj/quickjs-ng@${QUICKJS_VERSION}`;
|
|
53
|
+
var QUICKJS_ASSET_PATH = `/toolchains/quickjs-${QUICKJS_VERSION}.wasm.gz.bin`;
|
|
54
|
+
var QUICKJS_ASSET_SHA256 = "8c7f0588210490e7d77f198fc91f72c1b94787ab4c359c4786ca59a363c4f5e8";
|
|
55
|
+
var TYPESCRIPT_VERSION = "7.0.2";
|
|
56
|
+
var TYPESCRIPT_ASSET_PATH = `/toolchains/typescript-${TYPESCRIPT_VERSION}.wasm.gz.bin`;
|
|
57
|
+
var TYPESCRIPT_ASSET_SHA256 = "06e58ce887d95d1895055699b8dc96a1cde7d1f2baa48de40f9b790e3271dc16";
|
|
58
|
+
var RUST_VERSION = "1.91.1-dev";
|
|
59
|
+
var RUST_PACKAGE_ASSET_PATH = `/toolchains/rust-${RUST_VERSION}.webc.gz.bin`;
|
|
60
|
+
var RUST_PACKAGE_MANIFEST_ASSET_PATH = `/toolchains/rust-${RUST_VERSION}.manifest.json`;
|
|
61
|
+
var RUST_COMPRESSED_PACKAGE_SHA256 = "cfbdadc67be1315e735aa55bdf8a5a0d00171982a023fefcf7ba586127753887";
|
|
62
|
+
/** SHA-256 after browser-side gzip decompression. */
|
|
63
|
+
var RUST_PACKAGE_SHA256 = "765de8d68d03078e79f69f49dec0dcab1ff96fe3bbe5e9eafebb2ce61a39d3ee";
|
|
64
|
+
var RUST_PACKAGE_MANIFEST_SHA256 = "d5bbdca994e61888679c5738cb9420649c0854ed0eb5d65468bc67d5d550bce1";
|
|
65
|
+
var GO_VERSION = "1.26.5";
|
|
66
|
+
var GO_PACKAGE_ASSET_PATH = `/toolchains/go-${GO_VERSION}-wasip1.webc.gz.bin`;
|
|
67
|
+
var GO_PACKAGE_MANIFEST_ASSET_PATH = `/toolchains/go-${GO_VERSION}-wasip1.manifest.json`;
|
|
68
|
+
var GO_STANDARD_LIBRARY_ASSET_PATH = `/toolchains/go-${GO_VERSION}-wasip1.stdlib.gz.bin`;
|
|
69
|
+
var GO_COMPRESSED_PACKAGE_SHA256 = "70a7e359884b09b2e1a622d6ac5cd6e31c334aab519e6dd80dff5e040a9e09e4";
|
|
70
|
+
var GO_PACKAGE_SHA256 = "c3a97934b6a83fefdea5c31f99141f70f76189eedc9ec0fa9ccd41302e50963b";
|
|
71
|
+
var GO_PACKAGE_MANIFEST_SHA256 = "5d784e9ca640b9525e84b598c0beb97ca110ae908568ca45f6441a441d99a262";
|
|
72
|
+
var GO_COMPRESSED_STANDARD_LIBRARY_SHA256 = "aeffc384fdc624544f174ba5fc3c22395717fdbc3c4387d677d20855b6be80d8";
|
|
73
|
+
var GO_STANDARD_LIBRARY_SHA256 = "e1ec64b08efd02b35b7ffdaf3970875aaae98325fa4795eafca94ebae2d0d192";
|
|
74
|
+
var GO_COMPILER_SHA256 = "9e557f5b86961fd604217d7521461c5d2b7322e383fa30dead5669b77db12201";
|
|
75
|
+
var GO_LINKER_SHA256 = "2eefca10af935a307ab7946447146bd30e0ea0fb5460be54dc1c55844d155580";
|
|
76
|
+
var JAVA_VERSION = "teavm-0.13.1-wasi";
|
|
77
|
+
var JAVA_COMPILER_PACKAGE = `wasm-oj/java-teavm@${JAVA_VERSION}`;
|
|
78
|
+
var JAVA_COMPILER_ASSET_PATH = "/toolchains/java-teavm-0.13.1.wasi.compiler.webc.gz.bin";
|
|
79
|
+
var JAVA_COMPILE_CLASSLIB_ASSET_PATH = "/toolchains/java-teavm-0.13.1.compile-classlib.bin";
|
|
80
|
+
var JAVA_RUNTIME_CLASSLIB_ASSET_PATH = "/toolchains/java-teavm-0.13.1.runtime-classlib.bin";
|
|
81
|
+
var JAVA_COMPILER_COMPRESSED_PACKAGE_SHA256 = "129f1f51d591e58954f88787d36396b856a9a68ba3ae9c9d14f20bd67c2c7722";
|
|
82
|
+
var JAVA_COMPILER_PACKAGE_SHA256 = "f8f86761cf31062565187e4a66f73b6903f257fe84c0ce70ea1cd28441b6c2e9";
|
|
83
|
+
var JAVA_COMPILER_SHA256 = "33a0d662395256f10a5d02ea305fc7b18007b2b8fe8996859017b084d3d19735";
|
|
84
|
+
var JAVA_COMPILE_CLASSLIB_SHA256 = "acfe3fb09e5f2c0c7c8dc2339c66fcdadc1f8e1bf1c74be446926175ef770868";
|
|
85
|
+
var JAVA_RUNTIME_CLASSLIB_SHA256 = "21a9394586e416af2fca4eb0ed08521cbc8924e1d1afaa07863a59a3cfae54ab";
|
|
86
|
+
Object.freeze({
|
|
87
|
+
[CLANG_PACKAGE_ASSET_PATH]: CLANG_COMPRESSED_PACKAGE_SHA256,
|
|
88
|
+
[CLANG_CC1_PINS_ASSET_PATH]: CLANG_CC1_PINS_SHA256,
|
|
89
|
+
[CLANG_MANIFEST_ASSET_PATH]: CLANG_MANIFEST_SHA256,
|
|
90
|
+
[CLANG_LIBCXX_PCH_MANIFEST_ASSET_PATH]: CLANG_LIBCXX_PCH_MANIFEST_SHA256,
|
|
91
|
+
[CLANG_LIBCXX_PCH_DEBUG_ASSET_PATH]: CLANG_LIBCXX_PCH["cpp-debug"].compressedSha256,
|
|
92
|
+
[CLANG_LIBCXX_PCH_RELEASE_ASSET_PATH]: CLANG_LIBCXX_PCH["cpp-release"].compressedSha256,
|
|
93
|
+
[PYTHON_PACKAGE_ASSET_PATH]: PYTHON_COMPRESSED_PACKAGE_SHA256,
|
|
94
|
+
[PYTHON_PACKAGE_MANIFEST_ASSET_PATH]: PYTHON_PACKAGE_MANIFEST_SHA256,
|
|
95
|
+
[QUICKJS_ASSET_PATH]: QUICKJS_ASSET_SHA256,
|
|
96
|
+
[TYPESCRIPT_ASSET_PATH]: TYPESCRIPT_ASSET_SHA256,
|
|
97
|
+
[RUST_PACKAGE_ASSET_PATH]: RUST_COMPRESSED_PACKAGE_SHA256,
|
|
98
|
+
[RUST_PACKAGE_MANIFEST_ASSET_PATH]: RUST_PACKAGE_MANIFEST_SHA256,
|
|
99
|
+
[GO_PACKAGE_ASSET_PATH]: GO_COMPRESSED_PACKAGE_SHA256,
|
|
100
|
+
[GO_PACKAGE_MANIFEST_ASSET_PATH]: GO_PACKAGE_MANIFEST_SHA256,
|
|
101
|
+
[GO_STANDARD_LIBRARY_ASSET_PATH]: GO_COMPRESSED_STANDARD_LIBRARY_SHA256,
|
|
102
|
+
[JAVA_COMPILER_ASSET_PATH]: JAVA_COMPILER_COMPRESSED_PACKAGE_SHA256,
|
|
103
|
+
[JAVA_COMPILE_CLASSLIB_ASSET_PATH]: JAVA_COMPILE_CLASSLIB_SHA256,
|
|
104
|
+
[JAVA_RUNTIME_CLASSLIB_ASSET_PATH]: JAVA_RUNTIME_CLASSLIB_SHA256
|
|
105
|
+
});
|
|
106
|
+
var TOOLCHAIN_CONTENT_SHA256 = Object.freeze({
|
|
107
|
+
c: Object.freeze([
|
|
108
|
+
CLANG_COMPRESSED_PACKAGE_SHA256,
|
|
109
|
+
CLANG_PACKAGE_SHA256,
|
|
110
|
+
CLANG_CC1_PINS_SHA256,
|
|
111
|
+
CLANG_MANIFEST_SHA256
|
|
112
|
+
]),
|
|
113
|
+
cpp: Object.freeze([
|
|
114
|
+
CLANG_COMPRESSED_PACKAGE_SHA256,
|
|
115
|
+
CLANG_PACKAGE_SHA256,
|
|
116
|
+
CLANG_CC1_PINS_SHA256,
|
|
117
|
+
CLANG_MANIFEST_SHA256
|
|
118
|
+
]),
|
|
119
|
+
rust: Object.freeze([
|
|
120
|
+
RUST_COMPRESSED_PACKAGE_SHA256,
|
|
121
|
+
RUST_PACKAGE_SHA256,
|
|
122
|
+
RUST_PACKAGE_MANIFEST_SHA256
|
|
123
|
+
]),
|
|
124
|
+
python: Object.freeze([
|
|
125
|
+
PYTHON_COMPRESSED_PACKAGE_SHA256,
|
|
126
|
+
PYTHON_PACKAGE_SHA256,
|
|
127
|
+
PYTHON_PACKAGE_MANIFEST_SHA256,
|
|
128
|
+
PYTHON_RUNTIME_FILES_ARCHIVE_SHA256
|
|
129
|
+
]),
|
|
130
|
+
javascript: Object.freeze([TYPESCRIPT_ASSET_SHA256, QUICKJS_ASSET_SHA256]),
|
|
131
|
+
typescript: Object.freeze([TYPESCRIPT_ASSET_SHA256, QUICKJS_ASSET_SHA256]),
|
|
132
|
+
go: Object.freeze([
|
|
133
|
+
GO_COMPRESSED_PACKAGE_SHA256,
|
|
134
|
+
GO_PACKAGE_SHA256,
|
|
135
|
+
GO_PACKAGE_MANIFEST_SHA256,
|
|
136
|
+
GO_COMPRESSED_STANDARD_LIBRARY_SHA256,
|
|
137
|
+
GO_STANDARD_LIBRARY_SHA256,
|
|
138
|
+
GO_COMPILER_SHA256,
|
|
139
|
+
GO_LINKER_SHA256
|
|
140
|
+
])
|
|
141
|
+
});
|
|
142
|
+
/** Exact executable/compiler content used by cache keys and cost calibration. */
|
|
143
|
+
function toolchainContentIdentity(language) {
|
|
144
|
+
if (language === "java") return [
|
|
145
|
+
JAVA_COMPILER_COMPRESSED_PACKAGE_SHA256,
|
|
146
|
+
JAVA_COMPILER_PACKAGE_SHA256,
|
|
147
|
+
JAVA_COMPILER_SHA256,
|
|
148
|
+
JAVA_COMPILE_CLASSLIB_SHA256,
|
|
149
|
+
JAVA_RUNTIME_CLASSLIB_SHA256
|
|
150
|
+
].join(".");
|
|
151
|
+
if (!isBuiltinLanguage(language)) throw new Error(`WASM-OJ has no built-in toolchain for '${language}'.`);
|
|
152
|
+
return TOOLCHAIN_CONTENT_SHA256[language].join(".");
|
|
153
|
+
}
|
|
154
|
+
function freezeToolchain(definition) {
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
...definition,
|
|
157
|
+
compilerPackages: Object.freeze([...definition.compilerPackages]),
|
|
158
|
+
targets: Object.freeze([...definition.targets])
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
var TOOLCHAINS = Object.freeze({
|
|
162
|
+
c: freezeToolchain({
|
|
163
|
+
language: "c",
|
|
164
|
+
label: "Clang 22",
|
|
165
|
+
artifact: "wasm",
|
|
166
|
+
compilerPackages: [CLANG_PACKAGE],
|
|
167
|
+
targets: ["wasip1", "wasix"],
|
|
168
|
+
version: CLANG_VERSION,
|
|
169
|
+
note: "Native wasip1 module via Clang and LLD; directly executable by WASI and WASIX runtimes."
|
|
170
|
+
}),
|
|
171
|
+
cpp: freezeToolchain({
|
|
172
|
+
language: "cpp",
|
|
173
|
+
label: "Clang++ 22 (C++20)",
|
|
174
|
+
artifact: "wasm",
|
|
175
|
+
compilerPackages: [CLANG_PACKAGE],
|
|
176
|
+
targets: ["wasip1", "wasix"],
|
|
177
|
+
version: CLANG_VERSION,
|
|
178
|
+
note: "C++20/libc++ wasip1 module; directly executable by WASI and WASIX runtimes."
|
|
179
|
+
}),
|
|
180
|
+
rust: freezeToolchain({
|
|
181
|
+
language: "rust",
|
|
182
|
+
label: `Rust ${RUST_VERSION}`,
|
|
183
|
+
artifact: "wasm",
|
|
184
|
+
compilerPackages: [`rustc@${RUST_VERSION}#sha256:${RUST_PACKAGE_SHA256}`],
|
|
185
|
+
targets: ["wasip1"],
|
|
186
|
+
version: RUST_VERSION,
|
|
187
|
+
note: "Real rustc, its matching standard library, and a fresh deterministic wasm-ld stage execute as one pinned WebC under Wasmer."
|
|
188
|
+
}),
|
|
189
|
+
python: freezeToolchain({
|
|
190
|
+
language: "python",
|
|
191
|
+
label: `CPython ${PYTHON_VERSION}`,
|
|
192
|
+
artifact: "runtime-bundle",
|
|
193
|
+
compilerPackages: [PYTHON_PACKAGE],
|
|
194
|
+
runtimePackage: PYTHON_PACKAGE,
|
|
195
|
+
targets: ["wasip1"],
|
|
196
|
+
version: PYTHON_VERSION,
|
|
197
|
+
note: "Byte-compiled project bundled with the source-built CPython wasm32-wasip1 interpreter and standard library."
|
|
198
|
+
}),
|
|
199
|
+
javascript: freezeToolchain({
|
|
200
|
+
language: "javascript",
|
|
201
|
+
label: "QuickJS-ng",
|
|
202
|
+
artifact: "runtime-bundle",
|
|
203
|
+
compilerPackages: [`typescript@${TYPESCRIPT_VERSION}-wasi`],
|
|
204
|
+
runtimePackage: QUICKJS_PACKAGE,
|
|
205
|
+
targets: ["wasip1"],
|
|
206
|
+
version: QUICKJS_VERSION,
|
|
207
|
+
note: "JavaScript checked by TypeScript/WASI, then executed by the bundled QuickJS-ng/WASI runtime."
|
|
208
|
+
}),
|
|
209
|
+
typescript: freezeToolchain({
|
|
210
|
+
language: "typescript",
|
|
211
|
+
label: "TypeScript + QuickJS-ng",
|
|
212
|
+
artifact: "runtime-bundle",
|
|
213
|
+
compilerPackages: [`typescript@${TYPESCRIPT_VERSION}-wasi`],
|
|
214
|
+
runtimePackage: QUICKJS_PACKAGE,
|
|
215
|
+
targets: ["wasip1"],
|
|
216
|
+
version: TYPESCRIPT_VERSION,
|
|
217
|
+
note: "The native TypeScript compiler and QuickJS-ng runtime both execute as local WASI modules."
|
|
218
|
+
}),
|
|
219
|
+
go: freezeToolchain({
|
|
220
|
+
language: "go",
|
|
221
|
+
label: `Go ${GO_VERSION}`,
|
|
222
|
+
artifact: "wasm",
|
|
223
|
+
compilerPackages: [`go@${GO_VERSION}#sha256:${GO_PACKAGE_SHA256}`],
|
|
224
|
+
targets: ["wasip1"],
|
|
225
|
+
version: GO_VERSION,
|
|
226
|
+
note: "The standard Go compiler, linker, and pinned wasip1 standard library execute locally as one WebC under Wasmer."
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
function toolchainPackageIdentities(language) {
|
|
230
|
+
if (language === "java") return [JAVA_COMPILER_PACKAGE];
|
|
231
|
+
if (!isBuiltinLanguage(language)) throw new Error(`WASM-OJ has no built-in toolchain for '${language}'.`);
|
|
232
|
+
const toolchain = TOOLCHAINS[language];
|
|
233
|
+
return [.../* @__PURE__ */ new Set([...toolchain.compilerPackages, ...toolchain.runtimePackage ? [toolchain.runtimePackage] : []])];
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/core/project-files.ts
|
|
237
|
+
var PROJECT_SOURCE_LIMITS = Object.freeze({
|
|
238
|
+
files: 256,
|
|
239
|
+
bytesPerFile: 4194304,
|
|
240
|
+
totalBytes: 16777216
|
|
241
|
+
});
|
|
242
|
+
var UTF8_ENCODER = new TextEncoder();
|
|
243
|
+
/** Locale-independent ordering used anywhere file order can affect build output. */
|
|
244
|
+
function compareCanonicalPaths(left, right) {
|
|
245
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
246
|
+
}
|
|
247
|
+
function assertSafeRelativePath(path, label = "Project path") {
|
|
248
|
+
if (typeof path !== "string" || !path || path !== path.trim() || path.length > 4096) throw new Error(`${label} must be a non-empty, trimmed string of at most 4096 characters.`);
|
|
249
|
+
if (path.startsWith("/") || path.includes("\\") || path.includes("\0") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw new Error(`${label} '${path}' must be a normalized relative path that cannot escape the project.`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Validates a project file set and returns a fresh, locale-independently sorted
|
|
253
|
+
* array. Build hosts must use this order for filesystem creation and argv.
|
|
254
|
+
*/
|
|
255
|
+
function canonicalProjectFiles(files) {
|
|
256
|
+
if (!Array.isArray(files) || files.length === 0) throw new Error("A project must contain at least one source file.");
|
|
257
|
+
if (files.length > PROJECT_SOURCE_LIMITS.files) throw new Error(`A project cannot contain more than ${PROJECT_SOURCE_LIMITS.files} source files.`);
|
|
258
|
+
const seen = /* @__PURE__ */ new Set();
|
|
259
|
+
let totalBytes = 0;
|
|
260
|
+
return files.map((file, index) => {
|
|
261
|
+
if (!file || typeof file !== "object") throw new Error(`Project file ${index} is invalid.`);
|
|
262
|
+
assertSafeRelativePath(file.path, `Project file ${index} path`);
|
|
263
|
+
assertLanguageIdentifier(file.language);
|
|
264
|
+
if (typeof file.content !== "string") throw new Error(`Project file '${file.path}' content must be a string.`);
|
|
265
|
+
if (file.content.length > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
|
|
266
|
+
const contentBytes = UTF8_ENCODER.encode(file.content).byteLength;
|
|
267
|
+
if (contentBytes > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
|
|
268
|
+
totalBytes += contentBytes;
|
|
269
|
+
if (totalBytes > PROJECT_SOURCE_LIMITS.totalBytes) throw new Error(`Project sources exceed the ${PROJECT_SOURCE_LIMITS.totalBytes} byte total limit.`);
|
|
270
|
+
if (seen.has(file.path)) throw new Error(`Duplicate project path '${file.path}'.`);
|
|
271
|
+
seen.add(file.path);
|
|
272
|
+
return {
|
|
273
|
+
path: file.path,
|
|
274
|
+
language: file.language,
|
|
275
|
+
content: file.content
|
|
276
|
+
};
|
|
277
|
+
}).sort((left, right) => compareCanonicalPaths(left.path, right.path));
|
|
278
|
+
}
|
|
279
|
+
function canonicalFileEntries(files) {
|
|
280
|
+
if (!files || typeof files !== "object" || Array.isArray(files)) throw new Error("Runtime bundle files must be a record.");
|
|
281
|
+
return Object.entries(files).map(([path, contents]) => {
|
|
282
|
+
assertSafeRelativePath(path, "Runtime bundle path");
|
|
283
|
+
return [path, contents];
|
|
284
|
+
}).sort(([left], [right]) => compareCanonicalPaths(left, right));
|
|
285
|
+
}
|
|
286
|
+
function canonicalFileRecord(files) {
|
|
287
|
+
return Object.fromEntries(canonicalFileEntries(files));
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/core/sha256.ts
|
|
291
|
+
async function sha256Hex(value) {
|
|
292
|
+
const source = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
293
|
+
const bytes = new Uint8Array(source.byteLength);
|
|
294
|
+
bytes.set(source);
|
|
295
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
296
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/core/dependencies.ts
|
|
300
|
+
var MIB = 1048576;
|
|
301
|
+
/** Contract-level admission limits shared by dependency hosts and compilers. */
|
|
302
|
+
var DEPENDENCY_RESOLUTION_LIMITS = Object.freeze({
|
|
303
|
+
requirements: 128,
|
|
304
|
+
sourceFiles: 128,
|
|
305
|
+
sourceTextBytes: 8 * MIB,
|
|
306
|
+
hosts: 32,
|
|
307
|
+
roots: 512,
|
|
308
|
+
packages: 512,
|
|
309
|
+
referencesPerPackage: 512,
|
|
310
|
+
concurrency: 16,
|
|
311
|
+
metadataBytes: 8 * MIB,
|
|
312
|
+
packageBytes: 256 * MIB,
|
|
313
|
+
totalDownloadBytes: 512 * MIB,
|
|
314
|
+
archiveFiles: 16384,
|
|
315
|
+
unpackedBytes: 512 * MIB
|
|
316
|
+
});
|
|
317
|
+
Object.freeze({
|
|
318
|
+
packages: DEPENDENCY_RESOLUTION_LIMITS.packages,
|
|
319
|
+
filesPerPackage: DEPENDENCY_RESOLUTION_LIMITS.archiveFiles,
|
|
320
|
+
bytesPerFile: 64 * MIB,
|
|
321
|
+
totalBytes: DEPENDENCY_RESOLUTION_LIMITS.unpackedBytes
|
|
322
|
+
});
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/compiler/clang-pins.ts
|
|
325
|
+
var decoder$3 = new TextDecoder();
|
|
326
|
+
async function decodeClangPins(bytes) {
|
|
327
|
+
const digest = await sha256Hex(bytes);
|
|
328
|
+
if (digest !== "66c4604dccd3f89d8e1472bf4432367d7396cce4a01279b1a1db445f229dba72") throw new Error(`The pinned cc1 manifest drifted from its contract: expected ${CLANG_CC1_PINS_SHA256}, received ${digest}. Regenerate it with scripts/pin-clang-cc1-argv.mjs and update CLANG_CC1_PINS_SHA256.`);
|
|
329
|
+
const parsed = JSON.parse(decoder$3.decode(bytes));
|
|
330
|
+
if (parsed.schema !== WASM_OJ_SCHEMAS.clangPins) throw new Error(`Unsupported cc1 pin schema '${parsed.schema}'.`);
|
|
331
|
+
for (const key of [
|
|
332
|
+
"input",
|
|
333
|
+
"output",
|
|
334
|
+
"mainFileName",
|
|
335
|
+
"objects"
|
|
336
|
+
]) if (typeof parsed.placeholders?.[key] !== "string") throw new Error(`The cc1 pin manifest is missing the '${key}' placeholder.`);
|
|
337
|
+
if (!parsed.command || !parsed.linkerCommand) throw new Error("The cc1 pin manifest is missing a compiler or linker command.");
|
|
338
|
+
if (!parsed.source || !/^[a-f0-9]{64}$/.test(parsed.sourceSha256)) throw new Error("The cc1 pin manifest is missing its source toolchain identity.");
|
|
339
|
+
for (const [key, config] of Object.entries(parsed.configs)) if (!Array.isArray(config.cc1) || !Array.isArray(config.link) || config.cc1[0] !== "-cc1") throw new Error(`The cc1 pin manifest entry '${key}' is malformed.`);
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
342
|
+
function instantiateClangCc1(template, placeholders, source, objectPath) {
|
|
343
|
+
const basename = source.slice(source.lastIndexOf("/") + 1);
|
|
344
|
+
const inputPath = source.startsWith("/") ? source : `/project/${source}`;
|
|
345
|
+
return template.map((token) => {
|
|
346
|
+
if (token === placeholders.input) return inputPath;
|
|
347
|
+
if (token === placeholders.output) return objectPath;
|
|
348
|
+
if (token === placeholders.mainFileName) return basename;
|
|
349
|
+
return token;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function instantiateClangPch(template, placeholders, header, outputPath) {
|
|
353
|
+
return instantiateClangCc1(template.map((token) => token === "-emit-obj" ? "-emit-pch" : token === "c++" ? "c++-header" : token), placeholders, header, outputPath);
|
|
354
|
+
}
|
|
355
|
+
function instantiateClangLink(template, placeholders, objectPaths, outputPath) {
|
|
356
|
+
const argv = [];
|
|
357
|
+
for (const token of template) if (token === placeholders.objects) argv.push(...objectPaths);
|
|
358
|
+
else if (token === placeholders.output) argv.push(outputPath);
|
|
359
|
+
else argv.push(token);
|
|
360
|
+
return argv;
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/compiler/incremental-build-graph.ts
|
|
364
|
+
var BUILD_NODE_KINDS = Object.freeze([
|
|
365
|
+
"source",
|
|
366
|
+
"header",
|
|
367
|
+
"package",
|
|
368
|
+
"pch",
|
|
369
|
+
"object",
|
|
370
|
+
"link-result"
|
|
371
|
+
]);
|
|
372
|
+
/**
|
|
373
|
+
* Content-addressed source → header/package → PCH/object → link-result graph.
|
|
374
|
+
*
|
|
375
|
+
* Logical manifests remember which inputs a tool actually observed. Reuse
|
|
376
|
+
* rehashes every input and derives the structural node key again; no timestamp
|
|
377
|
+
* or host path participates in identity.
|
|
378
|
+
*/
|
|
379
|
+
var IncrementalBuildGraph = class {
|
|
380
|
+
limitBytes;
|
|
381
|
+
logical = /* @__PURE__ */ new Map();
|
|
382
|
+
nodes = /* @__PURE__ */ new Map();
|
|
383
|
+
blobs = /* @__PURE__ */ new Map();
|
|
384
|
+
storedBytes = 0;
|
|
385
|
+
generation = 0;
|
|
386
|
+
constructor(limitBytes) {
|
|
387
|
+
if (!Number.isSafeInteger(limitBytes) || limitBytes <= 0) throw new RangeError("Incremental build graph limit must be a positive safe integer.");
|
|
388
|
+
this.limitBytes = limitBytes;
|
|
389
|
+
}
|
|
390
|
+
async lookup(logicalKey, availableInputs) {
|
|
391
|
+
const manifest = this.logical.get(logicalKey);
|
|
392
|
+
if (!manifest) return void 0;
|
|
393
|
+
const inputs = [];
|
|
394
|
+
for (const dependency of manifest.dependencies) {
|
|
395
|
+
const input = availableInputs.get(dependency.identity);
|
|
396
|
+
if (!input || input.kind !== dependency.kind) return void 0;
|
|
397
|
+
inputs.push(input);
|
|
398
|
+
}
|
|
399
|
+
return this.lookupExact(manifest.kind, logicalKey, inputs);
|
|
400
|
+
}
|
|
401
|
+
async lookupExact(kind, logicalKey, inputs) {
|
|
402
|
+
const key = await structuralKey(kind, logicalKey, (await this.internInputs(inputs)).map((item) => item.key));
|
|
403
|
+
const node = this.nodes.get(key);
|
|
404
|
+
if (!node) return void 0;
|
|
405
|
+
const blob = this.blobs.get(node.digest);
|
|
406
|
+
if (!blob) return void 0;
|
|
407
|
+
this.blobs.delete(node.digest);
|
|
408
|
+
this.blobs.set(node.digest, blob);
|
|
409
|
+
return blob.slice();
|
|
410
|
+
}
|
|
411
|
+
async store(kind, logicalKey, inputs, output) {
|
|
412
|
+
if (output.byteLength > this.limitBytes) return false;
|
|
413
|
+
const canonicalInputs = canonicalizeInputs(inputs);
|
|
414
|
+
const dependencies = await this.internInputs(canonicalInputs);
|
|
415
|
+
const key = await structuralKey(kind, logicalKey, dependencies.map((item) => item.key));
|
|
416
|
+
const bytes = output.slice();
|
|
417
|
+
const digest = await sha256Hex(bytes);
|
|
418
|
+
const previousManifest = this.logical.get(logicalKey);
|
|
419
|
+
const previousNode = previousManifest ? this.nodes.get(previousManifest.nodeKey) : void 0;
|
|
420
|
+
let changed = false;
|
|
421
|
+
if (previousNode && (previousNode.key !== key || previousNode.digest !== digest)) {
|
|
422
|
+
this.logical.delete(logicalKey);
|
|
423
|
+
this.nodes.delete(previousNode.key);
|
|
424
|
+
if (![...this.logical.values()].some((manifest) => this.nodes.get(manifest.nodeKey)?.digest === previousNode.digest)) {
|
|
425
|
+
const removed = this.blobs.get(previousNode.digest);
|
|
426
|
+
if (removed) {
|
|
427
|
+
this.blobs.delete(previousNode.digest);
|
|
428
|
+
this.storedBytes -= removed.byteLength;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
changed = true;
|
|
432
|
+
}
|
|
433
|
+
changed = this.ensureCapacity(bytes.byteLength, digest) || changed;
|
|
434
|
+
if (!this.blobs.has(digest)) {
|
|
435
|
+
this.blobs.set(digest, bytes);
|
|
436
|
+
this.storedBytes += bytes.byteLength;
|
|
437
|
+
changed = true;
|
|
438
|
+
}
|
|
439
|
+
const nextNode = {
|
|
440
|
+
key,
|
|
441
|
+
kind,
|
|
442
|
+
identity: logicalKey,
|
|
443
|
+
digest,
|
|
444
|
+
dependencies: dependencies.map((item) => item.key),
|
|
445
|
+
byteLength: bytes.byteLength
|
|
446
|
+
};
|
|
447
|
+
const nextManifest = {
|
|
448
|
+
kind,
|
|
449
|
+
dependencies: canonicalInputs.map(({ kind: inputKind, identity }) => ({
|
|
450
|
+
kind: inputKind,
|
|
451
|
+
identity
|
|
452
|
+
})),
|
|
453
|
+
nodeKey: key
|
|
454
|
+
};
|
|
455
|
+
const currentNode = this.nodes.get(key);
|
|
456
|
+
const currentManifest = this.logical.get(logicalKey);
|
|
457
|
+
if (!sameNode(currentNode, nextNode) || !sameLogicalManifest(currentManifest, nextManifest)) changed = true;
|
|
458
|
+
this.nodes.set(key, nextNode);
|
|
459
|
+
this.logical.set(logicalKey, nextManifest);
|
|
460
|
+
if (changed) this.generation += 1;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
exportState() {
|
|
464
|
+
const entries = [];
|
|
465
|
+
const referencedDigests = /* @__PURE__ */ new Set();
|
|
466
|
+
for (const [logicalKey, manifest] of this.logical) {
|
|
467
|
+
const node = this.nodes.get(manifest.nodeKey);
|
|
468
|
+
if (!node) continue;
|
|
469
|
+
const output = this.blobs.get(node.digest);
|
|
470
|
+
if (!output) continue;
|
|
471
|
+
const inputs = node.dependencies.map((key) => {
|
|
472
|
+
const dependency = this.nodes.get(key);
|
|
473
|
+
if (!dependency) throw new Error(`Build graph dependency '${key}' is missing.`);
|
|
474
|
+
return {
|
|
475
|
+
kind: dependency.kind,
|
|
476
|
+
identity: dependency.identity,
|
|
477
|
+
digest: dependency.digest
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
entries.push({
|
|
481
|
+
kind: manifest.kind,
|
|
482
|
+
logicalKey,
|
|
483
|
+
inputs,
|
|
484
|
+
outputDigest: node.digest,
|
|
485
|
+
outputByteLength: output.byteLength
|
|
486
|
+
});
|
|
487
|
+
referencedDigests.add(node.digest);
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
manifest: {
|
|
491
|
+
schema: WASM_OJ_SCHEMAS.incrementalBuildGraph,
|
|
492
|
+
version: 2,
|
|
493
|
+
generation: this.generation,
|
|
494
|
+
entries: entries.sort((left, right) => compareText(left.logicalKey, right.logicalKey))
|
|
495
|
+
},
|
|
496
|
+
blobs: [...referencedDigests].sort().map((digest) => ({
|
|
497
|
+
digest,
|
|
498
|
+
bytes: this.blobs.get(digest)
|
|
499
|
+
}))
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async restoreState(state) {
|
|
503
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || Object.keys(state).sort().join(",") !== "blobs,manifest") throw new Error("Incremental build graph state does not use the active WASM-OJ contract.");
|
|
504
|
+
const { manifest } = state;
|
|
505
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest) || Object.keys(manifest).sort().join(",") !== "entries,generation,schema,version" || manifest.schema !== WASM_OJ_SCHEMAS.incrementalBuildGraph || manifest.version !== 2 || !Number.isSafeInteger(manifest.generation) || manifest.generation < 0 || !Array.isArray(manifest.entries) || !Array.isArray(state.blobs)) throw new Error("Incremental build graph manifest does not use the active WASM-OJ contract.");
|
|
506
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
507
|
+
let totalBytes = 0;
|
|
508
|
+
let previousDigest = "";
|
|
509
|
+
for (const candidate of state.blobs) {
|
|
510
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("Incremental build graph state contains a malformed blob.");
|
|
511
|
+
const blob = candidate;
|
|
512
|
+
if (Object.keys(blob).sort().join(",") !== "bytes,digest" || typeof blob.digest !== "string" || !/^[0-9a-f]{64}$/.test(blob.digest) || blob.digest <= previousDigest || !(blob.bytes instanceof Uint8Array) || await sha256Hex(blob.bytes) !== blob.digest) throw new Error("Incremental build graph state contains an invalid content-addressed blob.");
|
|
513
|
+
previousDigest = blob.digest;
|
|
514
|
+
totalBytes += blob.bytes.byteLength;
|
|
515
|
+
if (!Number.isSafeInteger(totalBytes) || totalBytes > this.limitBytes) throw new Error("Incremental build graph state exceeds its storage limit.");
|
|
516
|
+
blobs.set(blob.digest, blob.bytes);
|
|
517
|
+
}
|
|
518
|
+
const logicalKeys = /* @__PURE__ */ new Set();
|
|
519
|
+
const verified = [];
|
|
520
|
+
const referencedDigests = /* @__PURE__ */ new Set();
|
|
521
|
+
let previousLogicalKey = "";
|
|
522
|
+
for (const candidate of manifest.entries) {
|
|
523
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("Incremental build graph manifest contains a malformed entry.");
|
|
524
|
+
const entry = candidate;
|
|
525
|
+
if (Object.keys(entry).sort().join(",") !== "inputs,kind,logicalKey,outputByteLength,outputDigest") throw new Error("Incremental build graph manifest entry has an invalid shape.");
|
|
526
|
+
if (entry.kind !== "pch" && entry.kind !== "object" && entry.kind !== "link-result") throw new Error("Incremental build graph manifest entry has an invalid node kind.");
|
|
527
|
+
if (typeof entry.logicalKey !== "string" || !entry.logicalKey || entry.logicalKey !== entry.logicalKey.trim() || entry.logicalKey.length > 16384 || previousLogicalKey !== "" && compareText(previousLogicalKey, entry.logicalKey) >= 0) throw new Error("Incremental build graph manifest has an invalid or non-canonical logical key.");
|
|
528
|
+
previousLogicalKey = entry.logicalKey;
|
|
529
|
+
if (logicalKeys.has(entry.logicalKey)) throw new Error(`Incremental build graph manifest repeats '${entry.logicalKey}'.`);
|
|
530
|
+
logicalKeys.add(entry.logicalKey);
|
|
531
|
+
if (typeof entry.outputDigest !== "string" || !/^[0-9a-f]{64}$/.test(entry.outputDigest) || !Number.isSafeInteger(entry.outputByteLength) || entry.outputByteLength < 0) throw new Error(`Incremental build graph output '${entry.logicalKey}' has invalid metadata.`);
|
|
532
|
+
const output = blobs.get(entry.outputDigest);
|
|
533
|
+
if (!output || output.byteLength !== entry.outputByteLength) throw new Error(`Incremental build graph output '${entry.logicalKey}' is missing or has the wrong size.`);
|
|
534
|
+
referencedDigests.add(entry.outputDigest);
|
|
535
|
+
if (!Array.isArray(entry.inputs)) throw new Error(`Incremental build graph entry '${entry.logicalKey}' inputs must be an array.`);
|
|
536
|
+
const inputs = entry.inputs.map((input) => {
|
|
537
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`Incremental build graph entry '${entry.logicalKey}' has a malformed input.`);
|
|
538
|
+
const record = input;
|
|
539
|
+
if (Object.keys(record).sort().join(",") !== "digest,identity,kind" || !BUILD_NODE_KINDS.slice(0, 5).includes(record.kind) || typeof record.identity !== "string" || typeof record.digest !== "string") throw new Error(`Incremental build graph entry '${entry.logicalKey}' has an invalid input.`);
|
|
540
|
+
return {
|
|
541
|
+
kind: record.kind,
|
|
542
|
+
identity: record.identity,
|
|
543
|
+
digest: record.digest
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
if (canonicalizeInputs(inputs).some((input, index) => input.identity !== inputs[index]?.identity)) throw new Error(`Incremental build graph entry '${entry.logicalKey}' inputs are not canonical.`);
|
|
547
|
+
verified.push({
|
|
548
|
+
kind: entry.kind,
|
|
549
|
+
logicalKey: entry.logicalKey,
|
|
550
|
+
inputs,
|
|
551
|
+
output
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (referencedDigests.size !== blobs.size) throw new Error("Incremental build graph state contains an unreferenced content-addressed blob.");
|
|
555
|
+
this.clear();
|
|
556
|
+
for (const entry of verified) if (!await this.store(entry.kind, entry.logicalKey, entry.inputs, entry.output)) throw new Error(`Incremental build graph entry '${entry.logicalKey}' exceeds its storage limit.`);
|
|
557
|
+
this.generation = manifest.generation;
|
|
558
|
+
}
|
|
559
|
+
snapshot() {
|
|
560
|
+
return {
|
|
561
|
+
schema: WASM_OJ_SCHEMAS.incrementalBuildGraph,
|
|
562
|
+
nodes: [...this.nodes.values()].map((node) => ({
|
|
563
|
+
...node,
|
|
564
|
+
dependencies: [...node.dependencies]
|
|
565
|
+
})).sort((left, right) => compareText(left.key, right.key)),
|
|
566
|
+
storedBytes: this.storedBytes
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
clear() {
|
|
570
|
+
const changed = this.logical.size > 0 || this.nodes.size > 0 || this.blobs.size > 0;
|
|
571
|
+
this.logical.clear();
|
|
572
|
+
this.nodes.clear();
|
|
573
|
+
this.blobs.clear();
|
|
574
|
+
this.storedBytes = 0;
|
|
575
|
+
if (changed) this.generation += 1;
|
|
576
|
+
}
|
|
577
|
+
async internInputs(inputs) {
|
|
578
|
+
return Promise.all(canonicalizeInputs(inputs).map(async (input) => {
|
|
579
|
+
const digest = await inputDigest(input);
|
|
580
|
+
const key = await sha256Hex(JSON.stringify({
|
|
581
|
+
schema: WASM_OJ_SCHEMAS.incrementalBuildGraph,
|
|
582
|
+
kind: input.kind,
|
|
583
|
+
identity: input.identity,
|
|
584
|
+
digest
|
|
585
|
+
}));
|
|
586
|
+
const node = {
|
|
587
|
+
key,
|
|
588
|
+
kind: input.kind,
|
|
589
|
+
identity: input.identity,
|
|
590
|
+
digest,
|
|
591
|
+
dependencies: [],
|
|
592
|
+
byteLength: input.bytes?.byteLength ?? 0
|
|
593
|
+
};
|
|
594
|
+
this.nodes.set(key, node);
|
|
595
|
+
return node;
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
ensureCapacity(incomingBytes, incomingDigest) {
|
|
599
|
+
if (this.blobs.has(incomingDigest)) return false;
|
|
600
|
+
let changed = false;
|
|
601
|
+
while (this.storedBytes + incomingBytes > this.limitBytes && this.blobs.size > 0) {
|
|
602
|
+
const oldestDigest = this.blobs.keys().next().value;
|
|
603
|
+
const oldest = this.blobs.get(oldestDigest);
|
|
604
|
+
this.blobs.delete(oldestDigest);
|
|
605
|
+
this.storedBytes -= oldest.byteLength;
|
|
606
|
+
changed = true;
|
|
607
|
+
for (const [key, node] of this.nodes) {
|
|
608
|
+
if (node.digest !== oldestDigest || node.dependencies.length === 0) continue;
|
|
609
|
+
this.nodes.delete(key);
|
|
610
|
+
for (const [logicalKey, manifest] of this.logical) if (manifest.nodeKey === key) this.logical.delete(logicalKey);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return changed;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function sameNode(left, right) {
|
|
617
|
+
return left?.key === right.key && left.kind === right.kind && left.identity === right.identity && left.digest === right.digest && left.byteLength === right.byteLength && left.dependencies.length === right.dependencies.length && left.dependencies.every((dependency, index) => dependency === right.dependencies[index]);
|
|
618
|
+
}
|
|
619
|
+
function sameLogicalManifest(left, right) {
|
|
620
|
+
return left?.kind === right.kind && left.nodeKey === right.nodeKey && left.dependencies.length === right.dependencies.length && left.dependencies.every((dependency, index) => dependency.kind === right.dependencies[index]?.kind && dependency.identity === right.dependencies[index]?.identity);
|
|
621
|
+
}
|
|
622
|
+
function canonicalizeInputs(inputs) {
|
|
623
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const input of inputs) {
|
|
625
|
+
if (!input.identity || input.identity !== input.identity.trim() || input.identity.length > 16384) throw new Error("Build graph input identities must be non-empty, trimmed strings.");
|
|
626
|
+
if (byIdentity.has(input.identity)) throw new Error(`Duplicate build graph input '${input.identity}'.`);
|
|
627
|
+
if (input.bytes === void 0 === (input.digest === void 0)) throw new Error(`Build graph input '${input.identity}' must provide exactly one of bytes or digest.`);
|
|
628
|
+
byIdentity.set(input.identity, input);
|
|
629
|
+
}
|
|
630
|
+
return [...byIdentity.values()].sort((left, right) => compareText(left.identity, right.identity));
|
|
631
|
+
}
|
|
632
|
+
function compareText(left, right) {
|
|
633
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
634
|
+
}
|
|
635
|
+
async function inputDigest(input) {
|
|
636
|
+
if (input.bytes) return sha256Hex(input.bytes);
|
|
637
|
+
if (!/^[0-9a-f]{64}$/.test(input.digest)) throw new Error(`Build graph input '${input.identity}' has an invalid digest.`);
|
|
638
|
+
return input.digest;
|
|
639
|
+
}
|
|
640
|
+
function structuralKey(kind, logicalKey, dependencies) {
|
|
641
|
+
return sha256Hex(JSON.stringify({
|
|
642
|
+
schema: WASM_OJ_SCHEMAS.incrementalBuildGraph,
|
|
643
|
+
kind,
|
|
644
|
+
logicalKey,
|
|
645
|
+
dependencies
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/compiler/clang-object-cache.ts
|
|
650
|
+
var decoder$2 = new TextDecoder();
|
|
651
|
+
/**
|
|
652
|
+
* Content-addressed direct-mode cache for Clang translation units.
|
|
653
|
+
*
|
|
654
|
+
* Unit identity includes the frozen argv manifest and source digest. Every
|
|
655
|
+
* project dependency from Clang's dependency file is rehashed before reuse.
|
|
656
|
+
* System headers are covered by the pinned toolchain identity.
|
|
657
|
+
*/
|
|
658
|
+
var ClangObjectCache = class {
|
|
659
|
+
graph;
|
|
660
|
+
constructor(limitBytes) {
|
|
661
|
+
this.graph = new IncrementalBuildGraph(limitBytes);
|
|
662
|
+
}
|
|
663
|
+
async unitManifestKey(pins, configKey, source, sourceBytes) {
|
|
664
|
+
const config = pins.configs[configKey];
|
|
665
|
+
if (!config) throw new Error(`Unknown pinned Clang configuration '${configKey}'.`);
|
|
666
|
+
return sha256Hex(JSON.stringify({
|
|
667
|
+
schema: WASM_OJ_SCHEMAS.objectCache,
|
|
668
|
+
pinsSha256: CLANG_CC1_PINS_SHA256,
|
|
669
|
+
sourceToolchainSha256: pins.sourceSha256,
|
|
670
|
+
packageSha256: CLANG_PACKAGE_SHA256,
|
|
671
|
+
version: pins.version,
|
|
672
|
+
config: configKey,
|
|
673
|
+
cc1Argv: config.cc1,
|
|
674
|
+
unit: source,
|
|
675
|
+
source: await sha256Hex(sourceBytes)
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
async lookup(manifestKey, projectFiles, additionalInputs = []) {
|
|
679
|
+
return this.graph.lookup(manifestKey, availableInputs(projectFiles, additionalInputs));
|
|
680
|
+
}
|
|
681
|
+
async store(manifestKey, dependencyPaths, projectFiles, object, additionalInputs = []) {
|
|
682
|
+
const normalized = normalizeProjectDependencies(dependencyPaths, projectFiles);
|
|
683
|
+
if (!normalized) return false;
|
|
684
|
+
return this.graph.store("object", manifestKey, [...graphInputs(normalized, projectFiles), ...additionalInputs], object);
|
|
685
|
+
}
|
|
686
|
+
async lookupPch(manifestKey, projectFiles) {
|
|
687
|
+
return this.graph.lookup(manifestKey, availableInputs(projectFiles));
|
|
688
|
+
}
|
|
689
|
+
async storePch(manifestKey, dependencyPaths, projectFiles, pch) {
|
|
690
|
+
const normalized = normalizeProjectDependencies(dependencyPaths, projectFiles);
|
|
691
|
+
if (!normalized) return false;
|
|
692
|
+
return this.graph.store("pch", manifestKey, graphInputs(normalized, projectFiles), pch);
|
|
693
|
+
}
|
|
694
|
+
lookupLink(manifestKey, objects) {
|
|
695
|
+
return this.graph.lookupExact("link-result", manifestKey, withToolchain(objects));
|
|
696
|
+
}
|
|
697
|
+
storeLink(manifestKey, objects, wasm) {
|
|
698
|
+
return this.graph.store("link-result", manifestKey, withToolchain(objects), wasm);
|
|
699
|
+
}
|
|
700
|
+
snapshot() {
|
|
701
|
+
return this.graph.snapshot();
|
|
702
|
+
}
|
|
703
|
+
exportState() {
|
|
704
|
+
return this.graph.exportState();
|
|
705
|
+
}
|
|
706
|
+
restoreState(state) {
|
|
707
|
+
return this.graph.restoreState(state);
|
|
708
|
+
}
|
|
709
|
+
clear() {
|
|
710
|
+
this.graph.clear();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function parseClangDependencyFile(bytes) {
|
|
714
|
+
const joined = decoder$2.decode(bytes).replace(/\\\r?\n/g, " ");
|
|
715
|
+
const colon = joined.indexOf(":");
|
|
716
|
+
if (colon < 0) return [];
|
|
717
|
+
const deps = [];
|
|
718
|
+
const pattern = /(?:\\.|[^\s\\])+/g;
|
|
719
|
+
const remainder = joined.slice(colon + 1);
|
|
720
|
+
for (let match = pattern.exec(remainder); match; match = pattern.exec(remainder)) deps.push(match[0].replace(/\\(.)/g, "$1"));
|
|
721
|
+
return deps;
|
|
722
|
+
}
|
|
723
|
+
function normalizeProjectDependencies(dependencyPaths, projectFiles) {
|
|
724
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
725
|
+
for (const raw of dependencyPaths) {
|
|
726
|
+
if (raw.startsWith("/usr/") || raw.startsWith("/sysroot/") || raw.startsWith("/lib/")) continue;
|
|
727
|
+
const path = raw.startsWith("/project/") ? raw.slice(9) : raw.replace(/^\.\//, "");
|
|
728
|
+
if (!path || path.startsWith("/") || path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) return;
|
|
729
|
+
if (!projectFiles.has(path)) return void 0;
|
|
730
|
+
normalized.add(path);
|
|
731
|
+
}
|
|
732
|
+
if (normalized.size === 0) return void 0;
|
|
733
|
+
return [...normalized].sort();
|
|
734
|
+
}
|
|
735
|
+
function fileKind(path) {
|
|
736
|
+
return /\.(?:c|cc|cpp|cxx)$/i.test(path) ? "source" : "header";
|
|
737
|
+
}
|
|
738
|
+
function graphInputs(paths, projectFiles) {
|
|
739
|
+
return withToolchain(paths.map((path) => ({
|
|
740
|
+
kind: fileKind(path),
|
|
741
|
+
identity: path,
|
|
742
|
+
bytes: projectFiles.get(path)
|
|
743
|
+
})));
|
|
744
|
+
}
|
|
745
|
+
function withToolchain(inputs) {
|
|
746
|
+
return [...inputs, {
|
|
747
|
+
kind: "package",
|
|
748
|
+
identity: `cpp:clang@${CLANG_PACKAGE_SHA256}`,
|
|
749
|
+
digest: CLANG_PACKAGE_SHA256
|
|
750
|
+
}];
|
|
751
|
+
}
|
|
752
|
+
function availableInputs(projectFiles, additionalInputs = []) {
|
|
753
|
+
const inputs = new Map([...projectFiles].map(([path, bytes]) => [path, {
|
|
754
|
+
kind: fileKind(path),
|
|
755
|
+
identity: path,
|
|
756
|
+
bytes
|
|
757
|
+
}]));
|
|
758
|
+
inputs.set(`cpp:clang@${CLANG_PACKAGE_SHA256}`, {
|
|
759
|
+
kind: "package",
|
|
760
|
+
identity: `cpp:clang@${CLANG_PACKAGE_SHA256}`,
|
|
761
|
+
digest: CLANG_PACKAGE_SHA256
|
|
762
|
+
});
|
|
763
|
+
for (const input of additionalInputs) inputs.set(input.identity, input);
|
|
764
|
+
return inputs;
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/core/resources.ts
|
|
768
|
+
/** The weighted meter defined by the active WASM-OJ contract. */
|
|
769
|
+
var WEIGHTED_METER_MODEL$1 = "weighted";
|
|
770
|
+
Math.floor(Number.MAX_SAFE_INTEGER / 1e6);
|
|
771
|
+
Object.freeze({
|
|
772
|
+
instructionBudget: 1e10,
|
|
773
|
+
logicalTimeLimitMs: 6e4,
|
|
774
|
+
memoryLimitBytes: 268435456,
|
|
775
|
+
outputLimitBytes: 4194304,
|
|
776
|
+
filesystemWriteLimitBytes: 67108864,
|
|
777
|
+
filesystemEntryLimit: 4096,
|
|
778
|
+
wallTimeLimitMs: 6e4
|
|
779
|
+
});
|
|
780
|
+
new TextEncoder();
|
|
781
|
+
Object.freeze({
|
|
782
|
+
runtimeCoreWasmSha256: "92500f3a2e65fe6979e893179d8000e12d66822c160eeb779b0d4fe0a6b55603",
|
|
783
|
+
runtimeSourceRootSha256: "3ef42cb2c70e7013e4a6f9d4d7457a7071101795fbd3753efcd20c1ac338ebd5",
|
|
784
|
+
wasmerNativeVersion: "7.2.1",
|
|
785
|
+
wasmerSdkVersion: "0.10.0",
|
|
786
|
+
wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e",
|
|
787
|
+
wasmerWasixVersion: "0.702.1"
|
|
788
|
+
});
|
|
789
|
+
/**
|
|
790
|
+
* SHA-256 of `runtimeIdentityBytes()`.
|
|
791
|
+
* Release verification independently checks the component bytes before this
|
|
792
|
+
* identity is admitted into a calibrated release.
|
|
793
|
+
*/
|
|
794
|
+
var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "24c0bcff9820fbfd1fd4db1c57e2a866b83041409dd22b5b725688739bd3e223";
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/core/cost-profile.ts
|
|
797
|
+
function coordinates(language, target, optimization) {
|
|
798
|
+
assertLanguageIdentifier(language);
|
|
799
|
+
return [
|
|
800
|
+
"wasm-oj-cost",
|
|
801
|
+
`contract-${WASM_OJ_CONTRACT_VERSION}`,
|
|
802
|
+
encodeURIComponent(language),
|
|
803
|
+
target,
|
|
804
|
+
optimization
|
|
805
|
+
];
|
|
806
|
+
}
|
|
807
|
+
/** Stable identity for one calibrated compiler/runtime overhead profile. */
|
|
808
|
+
function costProfileId(language, target, optimization, downstreamToolchainContent) {
|
|
809
|
+
const content = isBuiltinLanguage(language) ? toolchainContentIdentity(language) : downstreamToolchainContent;
|
|
810
|
+
if (!content || !/^[A-Za-z0-9._-]+$/.test(content)) throw new Error(isBuiltinLanguage(language) ? `WASM-OJ toolchain content identity is invalid for '${language}'.` : `Downstream language '${language}' requires an explicit content identity using letters, digits, '.', '_' or '-'.`);
|
|
811
|
+
return [
|
|
812
|
+
...coordinates(language, target, optimization),
|
|
813
|
+
`content-${content}`,
|
|
814
|
+
`runtime-${WASM_OJ_RUNTIME_IDENTITY_SHA256}`,
|
|
815
|
+
WEIGHTED_METER_MODEL$1
|
|
816
|
+
].join(":");
|
|
817
|
+
}
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/core/diagnostics.ts
|
|
820
|
+
function severity(value) {
|
|
821
|
+
if (value === "warning") return "warning";
|
|
822
|
+
if (value === "note" || value === "info") return "info";
|
|
823
|
+
return "error";
|
|
824
|
+
}
|
|
825
|
+
function projectPath(path) {
|
|
826
|
+
return path.replace(/^file:\/\//, "").replace(/^\/?(?:workspace|project|work)\//, "").replace(/^\.\//, "");
|
|
827
|
+
}
|
|
828
|
+
function parseClangDiagnostics(output) {
|
|
829
|
+
const diagnostics = [];
|
|
830
|
+
const pattern = /^(.*?):(\d+):(\d+):\s+(fatal error|error|warning|note):\s+(.+?)(?:\s+\[([^\]]+)\])?$/gm;
|
|
831
|
+
let match;
|
|
832
|
+
while ((match = pattern.exec(output)) !== null) diagnostics.push({
|
|
833
|
+
file: projectPath(match[1]),
|
|
834
|
+
line: Number(match[2]),
|
|
835
|
+
column: Number(match[3]),
|
|
836
|
+
severity: severity(match[4].replace("fatal ", "")),
|
|
837
|
+
message: match[5],
|
|
838
|
+
source: "clang",
|
|
839
|
+
code: match[6]
|
|
840
|
+
});
|
|
841
|
+
return diagnostics;
|
|
842
|
+
}
|
|
843
|
+
function parsePythonDiagnostics(output) {
|
|
844
|
+
const diagnostics = [];
|
|
845
|
+
const lines = output.split(/\r?\n/);
|
|
846
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
847
|
+
const location = lines[index].match(/^\s*File "([^"]+)", line (\d+)/);
|
|
848
|
+
if (!location) continue;
|
|
849
|
+
let column = 1;
|
|
850
|
+
let message = "Python compilation failed";
|
|
851
|
+
const caret = (lines[index + 2] ?? "").indexOf("^");
|
|
852
|
+
if (caret >= 0) column = caret + 1;
|
|
853
|
+
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
|
|
854
|
+
const error = lines[cursor].match(/^([A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
855
|
+
if (error) {
|
|
856
|
+
message = `${error[1]}: ${error[2]}`;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
diagnostics.push({
|
|
861
|
+
file: projectPath(location[1]),
|
|
862
|
+
line: Number(location[2]),
|
|
863
|
+
column,
|
|
864
|
+
severity: "error",
|
|
865
|
+
message,
|
|
866
|
+
source: "python"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return diagnostics;
|
|
870
|
+
}
|
|
871
|
+
function parseTypeScriptDiagnostics(output) {
|
|
872
|
+
const diagnostics = [];
|
|
873
|
+
const pattern = /^(.*?)\((\d+),(\d+)\):\s+(error|warning|message)\s+TS(\d+):\s+(.+)$/gm;
|
|
874
|
+
let match;
|
|
875
|
+
while ((match = pattern.exec(output)) !== null) diagnostics.push({
|
|
876
|
+
severity: severity(match[4]),
|
|
877
|
+
message: match[6],
|
|
878
|
+
file: projectPath(match[1]),
|
|
879
|
+
line: Number(match[2]),
|
|
880
|
+
column: Number(match[3]),
|
|
881
|
+
source: "typescript",
|
|
882
|
+
code: `TS${match[5]}`
|
|
883
|
+
});
|
|
884
|
+
return diagnostics;
|
|
885
|
+
}
|
|
886
|
+
function parseRustDiagnostics(output) {
|
|
887
|
+
const diagnostics = [];
|
|
888
|
+
for (const line of output.split(/\r?\n/)) {
|
|
889
|
+
if (!line.startsWith("{")) continue;
|
|
890
|
+
let value;
|
|
891
|
+
try {
|
|
892
|
+
value = JSON.parse(line);
|
|
893
|
+
} catch {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (!value || typeof value !== "object") continue;
|
|
897
|
+
const record = value;
|
|
898
|
+
if (record.$message_type !== "diagnostic" || typeof record.message !== "string") continue;
|
|
899
|
+
const spans = Array.isArray(record.spans) ? record.spans : [];
|
|
900
|
+
const location = spans.find((candidate) => candidate && typeof candidate === "object" && candidate.is_primary === true) ?? spans.find((candidate) => candidate && typeof candidate === "object");
|
|
901
|
+
const code = record.code && typeof record.code === "object" ? record.code.code : void 0;
|
|
902
|
+
diagnostics.push({
|
|
903
|
+
severity: severity(typeof record.level === "string" ? record.level : "error"),
|
|
904
|
+
message: record.message,
|
|
905
|
+
file: projectPath(typeof location?.file_name === "string" ? location.file_name : "main.rs"),
|
|
906
|
+
line: typeof location?.line_start === "number" ? location.line_start : 1,
|
|
907
|
+
column: typeof location?.column_start === "number" ? location.column_start : 1,
|
|
908
|
+
endLine: typeof location?.line_end === "number" ? location.line_end : void 0,
|
|
909
|
+
endColumn: typeof location?.column_end === "number" ? location.column_end : void 0,
|
|
910
|
+
source: "rustc",
|
|
911
|
+
code: typeof code === "string" ? code : void 0
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return diagnostics;
|
|
915
|
+
}
|
|
916
|
+
function parseGoDiagnostics(output) {
|
|
917
|
+
const diagnostics = [];
|
|
918
|
+
const pattern = /^(.*?\.go):(\d+)(?::(\d+))?:\s*(.+)$/gm;
|
|
919
|
+
let match;
|
|
920
|
+
while ((match = pattern.exec(output)) !== null) diagnostics.push({
|
|
921
|
+
severity: "error",
|
|
922
|
+
message: match[4],
|
|
923
|
+
file: projectPath(match[1]),
|
|
924
|
+
line: Number(match[2]),
|
|
925
|
+
column: Number(match[3] ?? 1),
|
|
926
|
+
source: "go"
|
|
927
|
+
});
|
|
928
|
+
return diagnostics;
|
|
929
|
+
}
|
|
930
|
+
function ensureFailureDiagnostic(diagnostics, summary) {
|
|
931
|
+
if (diagnostics.length > 0) return diagnostics;
|
|
932
|
+
return [{
|
|
933
|
+
severity: "error",
|
|
934
|
+
file: projectPath(summary.file),
|
|
935
|
+
line: 1,
|
|
936
|
+
column: 1,
|
|
937
|
+
source: summary.source,
|
|
938
|
+
message: summary.message
|
|
939
|
+
}];
|
|
940
|
+
}
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region src/compiler/libcxx-pch.ts
|
|
943
|
+
var WASM_OJ_LIBCXX_PCH_HEADER = `#pragma once
|
|
944
|
+
#include <algorithm>
|
|
945
|
+
#include <array>
|
|
946
|
+
#include <bitset>
|
|
947
|
+
#include <cassert>
|
|
948
|
+
#include <cctype>
|
|
949
|
+
#include <cerrno>
|
|
950
|
+
#include <cfloat>
|
|
951
|
+
#include <charconv>
|
|
952
|
+
#include <chrono>
|
|
953
|
+
#include <climits>
|
|
954
|
+
#include <cmath>
|
|
955
|
+
#include <compare>
|
|
956
|
+
#include <concepts>
|
|
957
|
+
#include <cstddef>
|
|
958
|
+
#include <cstdint>
|
|
959
|
+
#include <cstdio>
|
|
960
|
+
#include <cstdlib>
|
|
961
|
+
#include <cstring>
|
|
962
|
+
#include <deque>
|
|
963
|
+
#include <exception>
|
|
964
|
+
#include <functional>
|
|
965
|
+
#include <iomanip>
|
|
966
|
+
#include <ios>
|
|
967
|
+
#include <iostream>
|
|
968
|
+
#include <iterator>
|
|
969
|
+
#include <limits>
|
|
970
|
+
#include <map>
|
|
971
|
+
#include <memory>
|
|
972
|
+
#include <numeric>
|
|
973
|
+
#include <optional>
|
|
974
|
+
#include <queue>
|
|
975
|
+
#include <random>
|
|
976
|
+
#include <ranges>
|
|
977
|
+
#include <set>
|
|
978
|
+
#include <span>
|
|
979
|
+
#include <sstream>
|
|
980
|
+
#include <stack>
|
|
981
|
+
#include <string>
|
|
982
|
+
#include <string_view>
|
|
983
|
+
#include <tuple>
|
|
984
|
+
#include <type_traits>
|
|
985
|
+
#include <unordered_map>
|
|
986
|
+
#include <unordered_set>
|
|
987
|
+
#include <utility>
|
|
988
|
+
#include <variant>
|
|
989
|
+
#include <vector>
|
|
990
|
+
`;
|
|
991
|
+
var decoder$1 = new TextDecoder();
|
|
992
|
+
async function decodeLibcxxPchManifest(bytes) {
|
|
993
|
+
const digest = await sha256Hex(bytes);
|
|
994
|
+
if (digest !== "d126c99e951a7302d4ea2b66da4ed64d3d74e9d319d562518867c8d8c97a06b8") throw new Error(`Pinned libc++ PCH manifest digest mismatch: expected ${CLANG_LIBCXX_PCH_MANIFEST_SHA256}, received ${digest}.`);
|
|
995
|
+
let value;
|
|
996
|
+
try {
|
|
997
|
+
value = JSON.parse(decoder$1.decode(bytes));
|
|
998
|
+
} catch (error) {
|
|
999
|
+
throw new Error("Pinned libc++ PCH manifest is not valid JSON.", { cause: error });
|
|
1000
|
+
}
|
|
1001
|
+
if (!isRecord$1(value) || value.schema !== WASM_OJ_SCHEMAS.clangLibcxxPch || value.version !== "22.0.0-git20542-10" || value.clangPackageSha256 !== "21ded33b9c6d4e1aaad5528c940bdaf6c3e84be77ea8f522f018ca7289a2a224" || value.clangPinsSha256 !== "66c4604dccd3f89d8e1472bf4432367d7396cce4a01279b1a1db445f229dba72" || value.header !== WASM_OJ_LIBCXX_PCH_HEADER || !isRecord$1(value.profiles)) throw new Error("Pinned libc++ PCH manifest is not admitted by the active Clang toolchain contract.");
|
|
1002
|
+
if (value.headerSha256 !== await sha256Hex(WASM_OJ_LIBCXX_PCH_HEADER)) throw new Error("Pinned libc++ PCH header digest does not match its canonical source.");
|
|
1003
|
+
const manifestProfiles = value.profiles;
|
|
1004
|
+
const profiles = Object.fromEntries(await Promise.all(["cpp-debug", "cpp-release"].map(async (profile) => {
|
|
1005
|
+
const asset = manifestProfiles[profile];
|
|
1006
|
+
if (!isRecord$1(asset) || typeof asset.path !== "string" || !asset.path.endsWith(`.${profile}.pch.gz.bin`) || !isBytes(asset.byteLength) || !isBytes(asset.compressedByteLength) || !isSha256(asset.sha256) || !isSha256(asset.compressedSha256)) throw new Error(`Pinned libc++ PCH profile '${profile}' is malformed.`);
|
|
1007
|
+
return [profile, {
|
|
1008
|
+
path: asset.path,
|
|
1009
|
+
byteLength: asset.byteLength,
|
|
1010
|
+
sha256: asset.sha256,
|
|
1011
|
+
compressedByteLength: asset.compressedByteLength,
|
|
1012
|
+
compressedSha256: asset.compressedSha256
|
|
1013
|
+
}];
|
|
1014
|
+
})));
|
|
1015
|
+
if (Object.keys(manifestProfiles).sort().join(",") !== "cpp-debug,cpp-release") throw new Error("Pinned libc++ PCH manifest has an unexpected profile set.");
|
|
1016
|
+
return {
|
|
1017
|
+
schema: WASM_OJ_SCHEMAS.clangLibcxxPch,
|
|
1018
|
+
version: CLANG_VERSION,
|
|
1019
|
+
clangPackageSha256: CLANG_PACKAGE_SHA256,
|
|
1020
|
+
clangPinsSha256: CLANG_CC1_PINS_SHA256,
|
|
1021
|
+
header: WASM_OJ_LIBCXX_PCH_HEADER,
|
|
1022
|
+
headerSha256: value.headerSha256,
|
|
1023
|
+
profiles
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function isToolchainLibcxxPchHeader(contents) {
|
|
1027
|
+
return contents === WASM_OJ_LIBCXX_PCH_HEADER;
|
|
1028
|
+
}
|
|
1029
|
+
function isRecord$1(value) {
|
|
1030
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1031
|
+
}
|
|
1032
|
+
function isSha256(value) {
|
|
1033
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
1034
|
+
}
|
|
1035
|
+
function isBytes(value) {
|
|
1036
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1037
|
+
}
|
|
1038
|
+
Object.freeze({
|
|
1039
|
+
randomSeed: 1592594996,
|
|
1040
|
+
realtimeEpochMs: Date.UTC(2e3, 0, 1),
|
|
1041
|
+
clockStepNs: 1e6
|
|
1042
|
+
});
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/runtime/determinism.ts
|
|
1045
|
+
var DETERMINISTIC_NATIVE_SOURCE_PATH = ".wasm-oj/determinism.c";
|
|
1046
|
+
var PYTHON_RUNNER_PATH = ".wasm-oj/deterministic_runner.py";
|
|
1047
|
+
var DETERMINISTIC_NATIVE_RUNTIME = String.raw`
|
|
1048
|
+
#include <stddef.h>
|
|
1049
|
+
#include <stdint.h>
|
|
1050
|
+
#include <stdlib.h>
|
|
1051
|
+
|
|
1052
|
+
#ifdef __cplusplus
|
|
1053
|
+
extern "C" {
|
|
1054
|
+
#endif
|
|
1055
|
+
|
|
1056
|
+
static uint32_t wasm_oj_random_state;
|
|
1057
|
+
static int wasm_oj_initialized;
|
|
1058
|
+
|
|
1059
|
+
static uint64_t wasm_oj_parse_u64(const char *value) {
|
|
1060
|
+
if (!value || !*value) abort();
|
|
1061
|
+
uint64_t result = 0;
|
|
1062
|
+
for (const unsigned char *cursor = (const unsigned char *)value; *cursor; ++cursor) {
|
|
1063
|
+
if (*cursor < '0' || *cursor > '9') abort();
|
|
1064
|
+
uint64_t digit = (uint64_t)(*cursor - '0');
|
|
1065
|
+
if (result > (UINT64_MAX - digit) / 10) abort();
|
|
1066
|
+
result = result * 10 + digit;
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
static void wasm_oj_initialize(void) {
|
|
1072
|
+
if (wasm_oj_initialized) return;
|
|
1073
|
+
uint64_t seed = wasm_oj_parse_u64(getenv("WASM_OJ_RANDOM_SEED"));
|
|
1074
|
+
wasm_oj_random_state = (uint32_t)seed;
|
|
1075
|
+
wasm_oj_initialized = 1;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
static uint32_t wasm_oj_next_u32(void) {
|
|
1079
|
+
wasm_oj_random_state += 0x9e3779b9u;
|
|
1080
|
+
uint32_t value = wasm_oj_random_state;
|
|
1081
|
+
value ^= value >> 16;
|
|
1082
|
+
value *= 0x21f0aaadu;
|
|
1083
|
+
value ^= value >> 15;
|
|
1084
|
+
value *= 0x735a2d97u;
|
|
1085
|
+
value ^= value >> 15;
|
|
1086
|
+
return value;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
uint32_t __imported_wasi_snapshot_preview1_random_get(uint8_t *buffer, size_t length) {
|
|
1090
|
+
wasm_oj_initialize();
|
|
1091
|
+
uint32_t word = 0;
|
|
1092
|
+
for (size_t index = 0; index < length; ++index) {
|
|
1093
|
+
if ((index & 3u) == 0) word = wasm_oj_next_u32();
|
|
1094
|
+
buffer[index] = (uint8_t)(word >> ((index & 3u) * 8u));
|
|
1095
|
+
}
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
#ifdef __cplusplus
|
|
1100
|
+
}
|
|
1101
|
+
#endif
|
|
1102
|
+
`;
|
|
1103
|
+
var PYTHON_DETERMINISTIC_RUNNER = String.raw`
|
|
1104
|
+
import os as _os
|
|
1105
|
+
import runpy as _runpy
|
|
1106
|
+
import sys as _sys
|
|
1107
|
+
|
|
1108
|
+
_random_state = None
|
|
1109
|
+
|
|
1110
|
+
def _random_seed():
|
|
1111
|
+
global _random_state
|
|
1112
|
+
if _random_state is None:
|
|
1113
|
+
_random_state = int(_os.environ["WASM_OJ_RANDOM_SEED"]) & 0xffffffff
|
|
1114
|
+
return _random_state
|
|
1115
|
+
|
|
1116
|
+
def _next_u32():
|
|
1117
|
+
global _random_state
|
|
1118
|
+
_random_seed()
|
|
1119
|
+
_random_state = (_random_state + 0x9e3779b9) & 0xffffffff
|
|
1120
|
+
value = _random_state
|
|
1121
|
+
value ^= value >> 16
|
|
1122
|
+
value = (value * 0x21f0aaad) & 0xffffffff
|
|
1123
|
+
value ^= value >> 15
|
|
1124
|
+
value = (value * 0x735a2d97) & 0xffffffff
|
|
1125
|
+
value ^= value >> 15
|
|
1126
|
+
return value & 0xffffffff
|
|
1127
|
+
|
|
1128
|
+
def _urandom(length):
|
|
1129
|
+
if not isinstance(length, int) or length < 0:
|
|
1130
|
+
raise ValueError("negative argument not allowed")
|
|
1131
|
+
output = bytearray(length)
|
|
1132
|
+
word = 0
|
|
1133
|
+
for index in range(length):
|
|
1134
|
+
if index % 4 == 0:
|
|
1135
|
+
word = _next_u32()
|
|
1136
|
+
output[index] = (word >> ((index % 4) * 8)) & 0xff
|
|
1137
|
+
return bytes(output)
|
|
1138
|
+
|
|
1139
|
+
_os.urandom = _urandom
|
|
1140
|
+
if hasattr(_os, "getrandom"):
|
|
1141
|
+
_os.getrandom = lambda size, flags=0: _urandom(size)
|
|
1142
|
+
|
|
1143
|
+
_entry = _sys.argv[1]
|
|
1144
|
+
_sys.argv = [_entry, *_sys.argv[2:]]
|
|
1145
|
+
_runpy.run_path(_entry, run_name="__main__")
|
|
1146
|
+
`;
|
|
1147
|
+
/**
|
|
1148
|
+
* Accept a mounted output only after byte-identical snapshots have persisted for
|
|
1149
|
+
* the complete stability interval. This prevents a valid WebAssembly prefix
|
|
1150
|
+
* from being mistaken for the command's final output.
|
|
1151
|
+
*/
|
|
1152
|
+
var MountedOutputStabilityObserver = class {
|
|
1153
|
+
#stabilityMs;
|
|
1154
|
+
#candidate;
|
|
1155
|
+
#candidateSince = 0;
|
|
1156
|
+
constructor(stabilityMs = 75) {
|
|
1157
|
+
if (!Number.isFinite(stabilityMs) || stabilityMs <= 0) throw new RangeError("Mounted-output stability interval must be positive and finite.");
|
|
1158
|
+
this.#stabilityMs = stabilityMs;
|
|
1159
|
+
}
|
|
1160
|
+
observe(snapshot, monotonicMs) {
|
|
1161
|
+
if (!Number.isFinite(monotonicMs)) throw new RangeError("Mounted-output observation time must be finite.");
|
|
1162
|
+
if (!snapshot) {
|
|
1163
|
+
this.#candidate = void 0;
|
|
1164
|
+
this.#candidateSince = 0;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (!this.#candidate || !bytesEqual(this.#candidate, snapshot)) {
|
|
1168
|
+
this.#candidate = snapshot.slice();
|
|
1169
|
+
this.#candidateSince = monotonicMs;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (monotonicMs - this.#candidateSince < this.#stabilityMs) return void 0;
|
|
1173
|
+
return snapshot.slice();
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
function bytesEqual(left, right) {
|
|
1177
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
1178
|
+
for (let index = 0; index < left.byteLength; index += 1) if (left[index] !== right[index]) return false;
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/compiler/dependency-input.ts
|
|
1183
|
+
var ECOSYSTEM_BY_LANGUAGE = Object.freeze({
|
|
1184
|
+
c: "cpp",
|
|
1185
|
+
cpp: "cpp",
|
|
1186
|
+
rust: "cargo",
|
|
1187
|
+
python: "pypi",
|
|
1188
|
+
javascript: "npm",
|
|
1189
|
+
typescript: "npm",
|
|
1190
|
+
go: "go"
|
|
1191
|
+
});
|
|
1192
|
+
function assertProjectDependencyEcosystem(project) {
|
|
1193
|
+
if (!project.dependencies) return;
|
|
1194
|
+
const expected = ECOSYSTEM_BY_LANGUAGE[project.config.language];
|
|
1195
|
+
if (!expected) throw new Error(`Compiler '${project.config.language}' does not declare a dependency ecosystem.`);
|
|
1196
|
+
const mismatch = project.dependencies.packages.find((item) => item.package.ecosystem !== expected);
|
|
1197
|
+
if (mismatch) throw new Error(`${project.config.language} projects accept only '${expected}' dependencies; '${mismatch.package.id}' is '${mismatch.package.ecosystem}'.`);
|
|
1198
|
+
}
|
|
1199
|
+
function projectDependencyPackages(project, ecosystem) {
|
|
1200
|
+
assertProjectDependencyEcosystem(project);
|
|
1201
|
+
return project.dependencies?.packages.filter((item) => item.package.ecosystem === ecosystem) ?? [];
|
|
1202
|
+
}
|
|
1203
|
+
function pythonDependencyFiles(project) {
|
|
1204
|
+
const sourceFiles = [];
|
|
1205
|
+
const artifactFiles = {};
|
|
1206
|
+
for (const item of projectDependencyPackages(project, "pypi")) for (const [path, bytes] of Object.entries(item.files)) {
|
|
1207
|
+
const installedPath = `site-packages/${path}`;
|
|
1208
|
+
artifactFiles[installedPath] = bytes.slice();
|
|
1209
|
+
if (path.endsWith(".py")) sourceFiles.push({
|
|
1210
|
+
path: installedPath,
|
|
1211
|
+
language: "python",
|
|
1212
|
+
content: decodeDependencyText(bytes, item.package.id, path)
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
sourceFiles,
|
|
1217
|
+
artifactFiles
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function npmDependencyFiles(project) {
|
|
1221
|
+
const files = {};
|
|
1222
|
+
const names = /* @__PURE__ */ new Set();
|
|
1223
|
+
const untyped = [];
|
|
1224
|
+
for (const item of projectDependencyPackages(project, "npm")) {
|
|
1225
|
+
if (names.has(item.package.name)) throw new Error(`npm dependency '${item.package.name}' resolves to multiple versions in a flat runtime graph.`);
|
|
1226
|
+
names.add(item.package.name);
|
|
1227
|
+
const manifestBytes = item.files["package.json"];
|
|
1228
|
+
if (!manifestBytes) throw new Error(`npm dependency '${item.package.id}' omits package.json.`);
|
|
1229
|
+
const manifestText = decodeDependencyText(manifestBytes, item.package.id, "package.json");
|
|
1230
|
+
if (JSON.parse(manifestText).type === "module") throw new Error(`npm dependency '${item.package.id}' uses unsupported ESM package semantics.`);
|
|
1231
|
+
if (!Object.keys(item.files).some((path) => path.endsWith(".d.ts"))) untyped.push(item.package.name);
|
|
1232
|
+
for (const [path, bytes] of Object.entries(item.files)) {
|
|
1233
|
+
const installedPath = `node_modules/${item.package.name}/${path}`;
|
|
1234
|
+
files[installedPath] = isNpmTextPath(path) ? decodeDependencyText(bytes, item.package.id, path) : bytes.slice();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (untyped.length > 0) files[".wasm-oj/npm-untyped-modules.d.ts"] = untyped.sort().flatMap((name) => [`declare module ${JSON.stringify(name)} { const value: any; export = value; }`, `declare module ${JSON.stringify(`${name}/*`)} { const value: any; export = value; }`]).join("\n");
|
|
1238
|
+
return files;
|
|
1239
|
+
}
|
|
1240
|
+
function cppDependencyInput(project) {
|
|
1241
|
+
const files = /* @__PURE__ */ new Map();
|
|
1242
|
+
const includeDirectories = [];
|
|
1243
|
+
const sources = [];
|
|
1244
|
+
for (const [index, item] of projectDependencyPackages(project, "cpp").entries()) {
|
|
1245
|
+
const root = `.wasm-oj/dependencies/cpp/${String(index).padStart(4, "0")}`;
|
|
1246
|
+
includeDirectories.push(`/project/${root}`);
|
|
1247
|
+
if (Object.keys(item.files).some((path) => path.startsWith("include/"))) includeDirectories.push(`/project/${root}/include`);
|
|
1248
|
+
for (const [path, bytes] of Object.entries(item.files)) {
|
|
1249
|
+
if (!isCppCompilerPath(path)) continue;
|
|
1250
|
+
decodeDependencyText(bytes, item.package.id, path);
|
|
1251
|
+
const installedPath = `${root}/${path}`;
|
|
1252
|
+
files.set(installedPath, bytes.slice());
|
|
1253
|
+
if (isCppSourcePath(path)) {
|
|
1254
|
+
if (project.config.language === "c" && !path.endsWith(".c")) throw new Error(`C dependency '${item.package.id}' contains C++ source '${path}'.`);
|
|
1255
|
+
sources.push(installedPath);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
files,
|
|
1261
|
+
includeDirectories: Object.freeze(includeDirectories),
|
|
1262
|
+
sources: Object.freeze(sources)
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function rustDependencyInput(project) {
|
|
1266
|
+
const packages = projectDependencyPackages(project, "cargo");
|
|
1267
|
+
const packageById = new Map(packages.map((item) => [item.package.id, item]));
|
|
1268
|
+
const ordered = topologicalPackages(packages);
|
|
1269
|
+
const descriptorById = /* @__PURE__ */ new Map();
|
|
1270
|
+
const files = [];
|
|
1271
|
+
for (const [index, item] of ordered.entries()) {
|
|
1272
|
+
const prefix = `.wasm-oj/dependencies/cargo/${String(index).padStart(4, "0")}`;
|
|
1273
|
+
const manifestBytes = item.files["Cargo.toml"];
|
|
1274
|
+
const manifest = decodeDependencyText(manifestBytes, item.package.id, "Cargo.toml");
|
|
1275
|
+
if (/\bpackage\s*=\s*"/m.test(dependencySections(manifest))) throw new Error(`Cargo dependency '${item.package.id}' uses unsupported renamed dependencies.`);
|
|
1276
|
+
const libSection = tomlSection(manifest, "lib");
|
|
1277
|
+
const crateName = rustIdentifier(tomlString(libSection, "name") ?? item.package.name, item.package.id);
|
|
1278
|
+
const rootRelative = tomlString(libSection, "path") ?? "src/lib.rs";
|
|
1279
|
+
if (!item.files[rootRelative]) throw new Error(`Cargo dependency '${item.package.id}' omits '${rootRelative}'.`);
|
|
1280
|
+
const edition = tomlString(tomlSection(manifest, "package"), "edition") ?? "2015";
|
|
1281
|
+
if (!/^(?:2015|2018|2021|2024)$/.test(edition)) throw new Error(`Cargo dependency '${item.package.id}' has unsupported Rust edition '${edition}'.`);
|
|
1282
|
+
const outputPath = `/work/build/deps/lib${String(index).padStart(4, "0")}_${crateName}.rlib`;
|
|
1283
|
+
descriptorById.set(item.package.id, {
|
|
1284
|
+
id: item.package.id,
|
|
1285
|
+
crateName,
|
|
1286
|
+
root: `${prefix}/${rootRelative}`,
|
|
1287
|
+
edition,
|
|
1288
|
+
outputPath,
|
|
1289
|
+
features: Object.freeze([...item.package.features ?? []])
|
|
1290
|
+
});
|
|
1291
|
+
for (const [path, bytes] of Object.entries(item.files)) {
|
|
1292
|
+
if (!isCargoCompilerText(path)) continue;
|
|
1293
|
+
files.push({
|
|
1294
|
+
path: `${prefix}/${path}`,
|
|
1295
|
+
language: "rust",
|
|
1296
|
+
content: decodeDependencyText(bytes, item.package.id, path)
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const crates = ordered.map((item) => {
|
|
1301
|
+
const descriptor = descriptorById.get(item.package.id);
|
|
1302
|
+
return Object.freeze({
|
|
1303
|
+
...descriptor,
|
|
1304
|
+
externs: Object.freeze(item.package.dependencies.map((id) => {
|
|
1305
|
+
const dependency = descriptorById.get(id);
|
|
1306
|
+
if (!dependency) throw new Error(`Cargo dependency '${item.package.id}' refers to unavailable '${id}'.`);
|
|
1307
|
+
return Object.freeze({
|
|
1308
|
+
crateName: dependency.crateName,
|
|
1309
|
+
path: dependency.outputPath
|
|
1310
|
+
});
|
|
1311
|
+
}))
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
const roots = project.dependencies?.lock.roots.map((id) => {
|
|
1315
|
+
if (!packageById.has(id)) throw new Error(`Cargo root '${id}' is unavailable.`);
|
|
1316
|
+
const descriptor = descriptorById.get(id);
|
|
1317
|
+
return Object.freeze({
|
|
1318
|
+
crateName: descriptor.crateName,
|
|
1319
|
+
path: descriptor.outputPath
|
|
1320
|
+
});
|
|
1321
|
+
}) ?? [];
|
|
1322
|
+
return {
|
|
1323
|
+
files,
|
|
1324
|
+
crates: Object.freeze(crates),
|
|
1325
|
+
roots: Object.freeze(roots)
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function goDependencyInput(project) {
|
|
1329
|
+
const files = [];
|
|
1330
|
+
const packages = [];
|
|
1331
|
+
for (const [moduleIndex, item] of projectDependencyPackages(project, "go").entries()) {
|
|
1332
|
+
const moduleRoot = `.wasm-oj/dependencies/go/${String(moduleIndex).padStart(4, "0")}`;
|
|
1333
|
+
const directories = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const [path, bytes] of Object.entries(item.files)) {
|
|
1335
|
+
if (!path.endsWith(".go") || path.endsWith("_test.go") || path.split("/").includes("vendor")) continue;
|
|
1336
|
+
const content = decodeDependencyText(bytes, item.package.id, path);
|
|
1337
|
+
if (/^\s*\/\/(?:go:build|\s*\+build)\b/m.test(content)) throw new Error(`Go dependency '${item.package.id}' uses unsupported build constraints in '${path}'.`);
|
|
1338
|
+
if (/import\s+(?:[._A-Za-z][A-Za-z0-9_]*\s+)?["`]C["`]/.test(content)) throw new Error(`Go dependency '${item.package.id}' uses unsupported cgo in '${path}'.`);
|
|
1339
|
+
const directory = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
1340
|
+
const group = directories.get(directory) ?? [];
|
|
1341
|
+
group.push({
|
|
1342
|
+
path,
|
|
1343
|
+
content
|
|
1344
|
+
});
|
|
1345
|
+
directories.set(directory, group);
|
|
1346
|
+
files.push({
|
|
1347
|
+
path: `${moduleRoot}/${path}`,
|
|
1348
|
+
language: "go",
|
|
1349
|
+
content
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
for (const [directory, sourceFiles] of [...directories].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1353
|
+
const packageNames = new Set(sourceFiles.map((file) => goPackageName(file.content, item.package.id, file.path)));
|
|
1354
|
+
if (packageNames.size !== 1) throw new Error(`Go dependency '${item.package.id}' directory '${directory || "."}' contains multiple packages.`);
|
|
1355
|
+
if ([...packageNames][0] === "main") continue;
|
|
1356
|
+
const importPath = directory ? `${item.package.name}/${directory}` : item.package.name;
|
|
1357
|
+
const index = packages.length;
|
|
1358
|
+
packages.push(Object.freeze({
|
|
1359
|
+
id: `${item.package.id}:${directory || "."}`,
|
|
1360
|
+
importPath,
|
|
1361
|
+
sourcePaths: Object.freeze(sourceFiles.map((file) => `${moduleRoot}/${file.path}`).sort()),
|
|
1362
|
+
imports: Object.freeze([...new Set(sourceFiles.flatMap((file) => goImports(file.content)))].sort()),
|
|
1363
|
+
archivePath: `/work/build/deps/${String(index).padStart(4, "0")}.a`
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
files,
|
|
1369
|
+
packages: Object.freeze(packages)
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function decodeDependencyText(bytes, id, path) {
|
|
1373
|
+
try {
|
|
1374
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
throw new Error(`Dependency '${id}' compiler file '${path}' is not valid UTF-8.`, { cause: error });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
function isNpmTextPath(path) {
|
|
1380
|
+
return /(?:^|\/)(?:package\.json)$/.test(path) || /\.(?:js|cjs|d\.ts|json)$/i.test(path);
|
|
1381
|
+
}
|
|
1382
|
+
function isCppCompilerPath(path) {
|
|
1383
|
+
return /\.(?:h|hh|hpp|hxx|inc|c|cc|cpp|cxx)$/i.test(path);
|
|
1384
|
+
}
|
|
1385
|
+
function isCppSourcePath(path) {
|
|
1386
|
+
return /\.(?:c|cc|cpp|cxx)$/i.test(path);
|
|
1387
|
+
}
|
|
1388
|
+
function topologicalPackages(packages) {
|
|
1389
|
+
const byId = new Map(packages.map((item) => [item.package.id, item]));
|
|
1390
|
+
const state = /* @__PURE__ */ new Map();
|
|
1391
|
+
const ordered = [];
|
|
1392
|
+
const visit = (id) => {
|
|
1393
|
+
const current = state.get(id);
|
|
1394
|
+
if (current === "visited") return;
|
|
1395
|
+
if (current === "visiting") throw new Error(`Dependency graph contains a cycle at '${id}'.`);
|
|
1396
|
+
const item = byId.get(id);
|
|
1397
|
+
if (!item) throw new Error(`Dependency graph refers to unavailable package '${id}'.`);
|
|
1398
|
+
state.set(id, "visiting");
|
|
1399
|
+
for (const dependency of item.package.dependencies) visit(dependency);
|
|
1400
|
+
state.set(id, "visited");
|
|
1401
|
+
ordered.push(item);
|
|
1402
|
+
};
|
|
1403
|
+
for (const item of packages) visit(item.package.id);
|
|
1404
|
+
return ordered;
|
|
1405
|
+
}
|
|
1406
|
+
function tomlSection(manifest, name) {
|
|
1407
|
+
return manifest.match(new RegExp(`(?:^|\\n)\\[${name.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\]\\s*\\n([\\s\\S]*?)(?=\\n\\[|$)`))?.[1] ?? "";
|
|
1408
|
+
}
|
|
1409
|
+
function tomlString(section, key) {
|
|
1410
|
+
return section.match(new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"\\s*(?:#.*)?$`, "m"))?.[1];
|
|
1411
|
+
}
|
|
1412
|
+
function dependencySections(manifest) {
|
|
1413
|
+
return manifest.split(/\n(?=\[)/).filter((section) => /\bdependencies\]/.test(section.split("\n", 1)[0])).join("\n");
|
|
1414
|
+
}
|
|
1415
|
+
function rustIdentifier(name, id) {
|
|
1416
|
+
const normalized = name.replaceAll("-", "_");
|
|
1417
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) throw new Error(`Cargo dependency '${id}' has unsupported crate name '${name}'.`);
|
|
1418
|
+
return normalized;
|
|
1419
|
+
}
|
|
1420
|
+
function isCargoCompilerText(path) {
|
|
1421
|
+
return path.endsWith(".rs") || path.endsWith(".md") || path.endsWith(".txt") || path === "Cargo.toml";
|
|
1422
|
+
}
|
|
1423
|
+
function goPackageName(source, id, path) {
|
|
1424
|
+
const match = source.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)\b/m);
|
|
1425
|
+
if (!match) throw new Error(`Go dependency '${id}' source '${path}' has no package declaration.`);
|
|
1426
|
+
return match[1];
|
|
1427
|
+
}
|
|
1428
|
+
function goImports(source) {
|
|
1429
|
+
const imports = [];
|
|
1430
|
+
const blockPattern = /\bimport\s*\(([\s\S]*?)\)/g;
|
|
1431
|
+
for (const block of source.matchAll(blockPattern)) for (const match of block[1].matchAll(/(?:^|\n)\s*(?:[._A-Za-z][A-Za-z0-9_]*\s+)?["`]([^"`]+)["`]/g)) imports.push(match[1]);
|
|
1432
|
+
const withoutBlocks = source.replace(blockPattern, "");
|
|
1433
|
+
for (const match of withoutBlocks.matchAll(/\bimport\s+(?:[._A-Za-z][A-Za-z0-9_]*\s+)?["`]([^"`]+)["`]/g)) imports.push(match[1]);
|
|
1434
|
+
return imports;
|
|
1435
|
+
}
|
|
1436
|
+
//#endregion
|
|
1437
|
+
//#region src/compiler/sdk-direct-clang.ts
|
|
1438
|
+
var loadedToolchain;
|
|
1439
|
+
var loadedLibcxxPchManifest;
|
|
1440
|
+
var loadedLibcxxPch = /* @__PURE__ */ new Map();
|
|
1441
|
+
var objectCache = new ClangObjectCache(67108864);
|
|
1442
|
+
var encoder$2 = new TextEncoder();
|
|
1443
|
+
var decoder = new TextDecoder();
|
|
1444
|
+
var STAGE_OUTPUT_TIMEOUT_MS = 55e3;
|
|
1445
|
+
/**
|
|
1446
|
+
* Browser compiler that drives the pinned cc1 and wasm-ld jobs through
|
|
1447
|
+
* the official SDK threadpool while keeping every command and project volume
|
|
1448
|
+
* isolated. No Clang driver or guest subprocess is involved.
|
|
1449
|
+
*/
|
|
1450
|
+
async function buildClangWithSdkDirect(project, cacheKey, requestId, host) {
|
|
1451
|
+
if (project.config.target !== "wasip1" && project.config.target !== "wasix") throw new Error("The output-ready Clang compiler accepts only wasip1 or wasix targets.");
|
|
1452
|
+
if (project.config.language !== "c" && project.config.language !== "cpp") throw new Error("The output-ready Clang compiler accepts only C and C++ projects.");
|
|
1453
|
+
const started = performance.now();
|
|
1454
|
+
host.progress(requestId, "loading-toolchain", "Loading pinned Clang 22 toolchain", .15);
|
|
1455
|
+
const { pins, compiler, linker } = await ensureToolchain(requestId, host);
|
|
1456
|
+
const configKey = `${project.config.language}-${project.config.optimization}`;
|
|
1457
|
+
const config = pins.configs[configKey];
|
|
1458
|
+
if (!config) throw new Error(`The pinned Clang manifest has no '${configKey}' configuration.`);
|
|
1459
|
+
host.trace(requestId, "filesystemPrepare", "start");
|
|
1460
|
+
const projectFiles = new Map(project.files.map((file) => [file.path, encoder$2.encode(file.content)]));
|
|
1461
|
+
const dependencies = cppDependencyInput(project);
|
|
1462
|
+
for (const [path, bytes] of dependencies.files) projectFiles.set(path, bytes);
|
|
1463
|
+
projectFiles.set(DETERMINISTIC_NATIVE_SOURCE_PATH, encoder$2.encode(DETERMINISTIC_NATIVE_RUNTIME));
|
|
1464
|
+
const directory = new Directory(Object.fromEntries([...projectFiles].map(([path, bytes]) => [`/${path}`, bytes])));
|
|
1465
|
+
try {
|
|
1466
|
+
await ensureDirectory(directory, "/build");
|
|
1467
|
+
await ensureDirectory(directory, "/.wasm-oj");
|
|
1468
|
+
host.trace(requestId, "filesystemPrepare", "end");
|
|
1469
|
+
const isCpp = project.config.language === "cpp";
|
|
1470
|
+
const extensions = isCpp ? /\.(?:cc|cpp|cxx)$/ : /\.c$/;
|
|
1471
|
+
const sources = project.files.filter((file) => extensions.test(file.path)).map((file) => file.path);
|
|
1472
|
+
if (!sources.includes(project.config.entry)) sources.unshift(project.config.entry);
|
|
1473
|
+
const units = [
|
|
1474
|
+
...sources,
|
|
1475
|
+
...dependencies.sources,
|
|
1476
|
+
DETERMINISTIC_NATIVE_SOURCE_PATH
|
|
1477
|
+
];
|
|
1478
|
+
let stdout = "";
|
|
1479
|
+
let stderr = "";
|
|
1480
|
+
const objectPaths = [];
|
|
1481
|
+
const objectInputs = [];
|
|
1482
|
+
let objectCacheHits = 0;
|
|
1483
|
+
let objectCacheStores = 0;
|
|
1484
|
+
let pchHits = 0;
|
|
1485
|
+
let pchMisses = 0;
|
|
1486
|
+
let pchStores = 0;
|
|
1487
|
+
let linkHits = 0;
|
|
1488
|
+
let linkMisses = 0;
|
|
1489
|
+
let linkStores = 0;
|
|
1490
|
+
const structuredDiagnostics = [];
|
|
1491
|
+
const pchHeader = isCpp ? findPrecompiledHeader(project) : void 0;
|
|
1492
|
+
const pchPath = "/project/build/wasm-oj.pch";
|
|
1493
|
+
let pchInput;
|
|
1494
|
+
let admittedPch = false;
|
|
1495
|
+
if (pchHeader) {
|
|
1496
|
+
const headerBytes = projectFiles.get(pchHeader);
|
|
1497
|
+
let pch;
|
|
1498
|
+
if (isToolchainLibcxxPchHeader(decoder.decode(headerBytes))) {
|
|
1499
|
+
admittedPch = true;
|
|
1500
|
+
const reservedHeader = "wasm-oj.libcxx.hpp";
|
|
1501
|
+
if (projectFiles.has(reservedHeader)) throw new Error(`C++ projects using WASM-OJ's admitted libc++ PCH may not define reserved path '${reservedHeader}'.`);
|
|
1502
|
+
projectFiles.set(reservedHeader, headerBytes);
|
|
1503
|
+
await directory.writeFile(`/${reservedHeader}`, headerBytes);
|
|
1504
|
+
pch = await loadLibcxxPch(configKey, requestId, host);
|
|
1505
|
+
pchHits += 1;
|
|
1506
|
+
await directory.writeFile(pchPath.slice(8), pch);
|
|
1507
|
+
} else {
|
|
1508
|
+
const baseKey = await objectCache.unitManifestKey(pins, configKey, pchHeader, headerBytes);
|
|
1509
|
+
const pchManifestKey = await sha256Hex(JSON.stringify({
|
|
1510
|
+
baseKey,
|
|
1511
|
+
mode: "c++-header"
|
|
1512
|
+
}));
|
|
1513
|
+
const cached = await objectCache.lookupPch(pchManifestKey, projectFiles);
|
|
1514
|
+
if (cached) {
|
|
1515
|
+
pch = cached;
|
|
1516
|
+
pchHits += 1;
|
|
1517
|
+
await directory.writeFile(pchPath.slice(8), pch);
|
|
1518
|
+
} else {
|
|
1519
|
+
pchMisses += 1;
|
|
1520
|
+
const dependencyPath = "/project/build/wasm-oj.pch.d";
|
|
1521
|
+
const args = instantiateClangPch(config.cc1, pins.placeholders, pchHeader, pchPath);
|
|
1522
|
+
args.splice(args.length - 1, 0, ...dependencies.includeDirectories.flatMap((directory) => ["-I", directory]));
|
|
1523
|
+
args.push("-dependency-file", dependencyPath, "-MT", pchPath);
|
|
1524
|
+
const output = await runPchStage(compiler, args, directory, host, requestId, pchPath, dependencyPath);
|
|
1525
|
+
structuredDiagnostics.push(...output.diagnostics);
|
|
1526
|
+
stdout += output.stdout;
|
|
1527
|
+
stderr += output.stderr;
|
|
1528
|
+
if (!output.pch || !output.dependency) return failedBuild(project, stdout, stderr, 1, "clang", structuredDiagnostics);
|
|
1529
|
+
pch = output.pch;
|
|
1530
|
+
if (await objectCache.storePch(pchManifestKey, parseClangDependencyFile(output.dependency), projectFiles, pch)) pchStores += 1;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
pchInput = {
|
|
1534
|
+
kind: "pch",
|
|
1535
|
+
identity: `pch:${pchHeader}`,
|
|
1536
|
+
digest: await sha256Hex(pch)
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
host.progress(requestId, "compiling", `Compiling ${units.length} translation units with SDK-direct cc1`, .35);
|
|
1540
|
+
host.trace(requestId, "commandStart", "start");
|
|
1541
|
+
host.trace(requestId, "commandStart", "end");
|
|
1542
|
+
host.trace(requestId, "commandWait", "start");
|
|
1543
|
+
host.trace(requestId, "projectCompile", "start");
|
|
1544
|
+
for (const [index, source] of units.entries()) {
|
|
1545
|
+
if (source === ".wasm-oj/determinism.c") {
|
|
1546
|
+
host.trace(requestId, "projectCompile", "end");
|
|
1547
|
+
host.trace(requestId, "runtimeShimCompile", "start");
|
|
1548
|
+
}
|
|
1549
|
+
const objectPath = `/project/build/${String(index).padStart(4, "0")}.o`;
|
|
1550
|
+
const dependencyPath = `/project/build/${String(index).padStart(4, "0")}.d`;
|
|
1551
|
+
const sourceBytes = projectFiles.get(source);
|
|
1552
|
+
if (!sourceBytes) throw new Error(`SDK-direct Clang is missing source bytes for '${source}'.`);
|
|
1553
|
+
const baseManifestKey = await objectCache.unitManifestKey(pins, configKey, source, sourceBytes);
|
|
1554
|
+
const unitPchInput = source !== ".wasm-oj/determinism.c" ? pchInput : void 0;
|
|
1555
|
+
const manifestKey = unitPchInput ? await sha256Hex(JSON.stringify({
|
|
1556
|
+
baseManifestKey,
|
|
1557
|
+
pch: unitPchInput.digest
|
|
1558
|
+
})) : baseManifestKey;
|
|
1559
|
+
const additionalInputs = unitPchInput ? [unitPchInput] : [];
|
|
1560
|
+
const cached = await objectCache.lookup(manifestKey, projectFiles, additionalInputs);
|
|
1561
|
+
if (cached) {
|
|
1562
|
+
await directory.writeFile(objectPath.slice(8), cached);
|
|
1563
|
+
objectCacheHits += 1;
|
|
1564
|
+
objectPaths.push(objectPath);
|
|
1565
|
+
objectInputs.push({
|
|
1566
|
+
kind: "object",
|
|
1567
|
+
identity: source,
|
|
1568
|
+
bytes: cached
|
|
1569
|
+
});
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
const args = instantiateClangCc1(config.cc1, pins.placeholders, source, objectPath);
|
|
1573
|
+
args.splice(args.length - 1, 0, ...dependencies.includeDirectories.flatMap((directory) => ["-I", directory]));
|
|
1574
|
+
if (unitPchInput) args.splice(args.length - 1, 0, "-include-pch", pchPath, ...admittedPch ? ["-fno-validate-pch"] : []);
|
|
1575
|
+
args.push("-dependency-file", dependencyPath, "-MT", objectPath);
|
|
1576
|
+
const output = await runClangStage(compiler, args, directory, host, requestId, source === ".wasm-oj/determinism.c" ? "runtimeShimSpawn" : "projectSpawn", source === ".wasm-oj/determinism.c" ? "runtimeShimWait" : "projectWait", source === ".wasm-oj/determinism.c" ? "runtimeShimOutputReady" : "projectOutputReady", objectPath, dependencyPath);
|
|
1577
|
+
structuredDiagnostics.push(...output.diagnostics);
|
|
1578
|
+
stdout += output.stdout;
|
|
1579
|
+
stderr += output.stderr;
|
|
1580
|
+
if (!output.object || !output.dependency) {
|
|
1581
|
+
host.trace(requestId, source === ".wasm-oj/determinism.c" ? "runtimeShimCompile" : "projectCompile", "end");
|
|
1582
|
+
host.trace(requestId, "commandWait", "end");
|
|
1583
|
+
return failedBuild(project, stdout, stderr, 1, "clang", structuredDiagnostics);
|
|
1584
|
+
}
|
|
1585
|
+
if (output.diagnostics.length === 0 && await objectCache.store(manifestKey, parseClangDependencyFile(output.dependency), projectFiles, output.object, additionalInputs)) objectCacheStores += 1;
|
|
1586
|
+
objectPaths.push(objectPath);
|
|
1587
|
+
objectInputs.push({
|
|
1588
|
+
kind: "object",
|
|
1589
|
+
identity: source,
|
|
1590
|
+
bytes: output.object
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
host.trace(requestId, "runtimeShimCompile", "end");
|
|
1594
|
+
host.progress(requestId, "linking", "Linking SDK-direct Clang objects", .8);
|
|
1595
|
+
host.trace(requestId, "link", "start");
|
|
1596
|
+
const outputPath = "/project/build/app.wasm";
|
|
1597
|
+
const linkArguments = instantiateClangLink(config.link, pins.placeholders, objectPaths, outputPath);
|
|
1598
|
+
const linkManifestKey = await sha256Hex(JSON.stringify({
|
|
1599
|
+
pins: pins.sourceSha256,
|
|
1600
|
+
package: CLANG_PACKAGE_SHA256,
|
|
1601
|
+
target: project.config.target,
|
|
1602
|
+
arguments: config.link
|
|
1603
|
+
}));
|
|
1604
|
+
let bytes = await objectCache.lookupLink(linkManifestKey, objectInputs);
|
|
1605
|
+
let linkedStdout = "";
|
|
1606
|
+
let linkedStderr = "";
|
|
1607
|
+
if (bytes) linkHits += 1;
|
|
1608
|
+
else {
|
|
1609
|
+
linkMisses += 1;
|
|
1610
|
+
const linked = await runLinkStage(linker, linkArguments, directory, host, requestId, "linkSpawn", "linkWait", "linkOutputReady", outputPath);
|
|
1611
|
+
linkedStdout = linked.stdout;
|
|
1612
|
+
linkedStderr = linked.stderr;
|
|
1613
|
+
bytes = linked.value;
|
|
1614
|
+
if (bytes && await objectCache.storeLink(linkManifestKey, objectInputs, bytes)) linkStores += 1;
|
|
1615
|
+
}
|
|
1616
|
+
host.trace(requestId, "link", "end");
|
|
1617
|
+
host.trace(requestId, "commandWait", "end");
|
|
1618
|
+
stdout += linkedStdout;
|
|
1619
|
+
stderr += linkedStderr;
|
|
1620
|
+
if (!bytes) return failedBuild(project, stdout, stderr, 1, "wasm-ld", structuredDiagnostics);
|
|
1621
|
+
host.progress(requestId, "linking", "Reading linked WebAssembly module", .95);
|
|
1622
|
+
host.trace(requestId, "artifactReadback", "start");
|
|
1623
|
+
host.trace(requestId, "artifactReadback", "end");
|
|
1624
|
+
return {
|
|
1625
|
+
success: true,
|
|
1626
|
+
diagnostics: structuredDiagnostics,
|
|
1627
|
+
artifact: {
|
|
1628
|
+
kind: "wasm",
|
|
1629
|
+
wasmOjContract: WASM_OJ_CONTRACT_VERSION,
|
|
1630
|
+
id: crypto.randomUUID(),
|
|
1631
|
+
projectId: project.id,
|
|
1632
|
+
cacheKey,
|
|
1633
|
+
name: `${project.name}.wasm`,
|
|
1634
|
+
language: project.config.language,
|
|
1635
|
+
target: project.config.target,
|
|
1636
|
+
optimization: project.config.optimization,
|
|
1637
|
+
createdAt: Date.now(),
|
|
1638
|
+
durationMs: performance.now() - started,
|
|
1639
|
+
size: bytes.byteLength,
|
|
1640
|
+
toolchains: toolchainPackageIdentities(project.config.language),
|
|
1641
|
+
costProfile: costProfileId(project.config.language, project.config.target, project.config.optimization),
|
|
1642
|
+
...project.dependencies === void 0 ? {} : { dependencyLockSha256: project.dependencies.lockSha256 },
|
|
1643
|
+
bytes
|
|
1644
|
+
},
|
|
1645
|
+
stdout,
|
|
1646
|
+
stderr,
|
|
1647
|
+
cacheHit: false,
|
|
1648
|
+
buildGraph: {
|
|
1649
|
+
hits: {
|
|
1650
|
+
pch: pchHits,
|
|
1651
|
+
object: objectCacheHits,
|
|
1652
|
+
"link-result": linkHits
|
|
1653
|
+
},
|
|
1654
|
+
misses: {
|
|
1655
|
+
pch: pchMisses,
|
|
1656
|
+
object: units.length - objectCacheHits,
|
|
1657
|
+
"link-result": linkMisses
|
|
1658
|
+
},
|
|
1659
|
+
stores: {
|
|
1660
|
+
pch: pchStores,
|
|
1661
|
+
object: objectCacheStores,
|
|
1662
|
+
"link-result": linkStores
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
} finally {
|
|
1667
|
+
directory.free();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function clearSdkDirectClangCaches() {
|
|
1671
|
+
await disposeSdkDirectClangToolchain();
|
|
1672
|
+
loadedLibcxxPchManifest = void 0;
|
|
1673
|
+
loadedLibcxxPch.clear();
|
|
1674
|
+
objectCache.clear();
|
|
1675
|
+
}
|
|
1676
|
+
/** Release all SDK resources tied to one Runtime while preserving object-cache bytes. */
|
|
1677
|
+
async function disposeSdkDirectClangToolchain() {
|
|
1678
|
+
const pending = loadedToolchain;
|
|
1679
|
+
loadedToolchain = void 0;
|
|
1680
|
+
if (!pending) return;
|
|
1681
|
+
const { pkg, compiler, linker } = await pending;
|
|
1682
|
+
compiler.free();
|
|
1683
|
+
linker.free();
|
|
1684
|
+
pkg.free();
|
|
1685
|
+
}
|
|
1686
|
+
async function ensureToolchain(requestId, host) {
|
|
1687
|
+
if (loadedToolchain) {
|
|
1688
|
+
for (const operation of [
|
|
1689
|
+
"toolchainFetch",
|
|
1690
|
+
"toolchainDecode",
|
|
1691
|
+
"toolchainLoad"
|
|
1692
|
+
]) {
|
|
1693
|
+
host.trace(requestId, operation, "start");
|
|
1694
|
+
host.trace(requestId, operation, "end");
|
|
1695
|
+
}
|
|
1696
|
+
return loadedToolchain;
|
|
1697
|
+
}
|
|
1698
|
+
loadedToolchain = (async () => {
|
|
1699
|
+
host.trace(requestId, "toolchainFetch", "start");
|
|
1700
|
+
const [packageBytes, pinsBytes] = await Promise.all([host.loadToolchainAsset(CLANG_PACKAGE_ASSET_PATH), host.loadToolchainFile(CLANG_CC1_PINS_ASSET_PATH)]);
|
|
1701
|
+
host.trace(requestId, "toolchainFetch", "end");
|
|
1702
|
+
host.trace(requestId, "toolchainDecode", "start");
|
|
1703
|
+
const pins = await decodeClangPins(pinsBytes);
|
|
1704
|
+
const packageSha256 = await sha256Hex(packageBytes);
|
|
1705
|
+
if (packageSha256 !== "21ded33b9c6d4e1aaad5528c940bdaf6c3e84be77ea8f522f018ca7289a2a224") throw new Error(`Pinned Clang package digest mismatch: received ${packageSha256}.`);
|
|
1706
|
+
host.trace(requestId, "toolchainDecode", "end");
|
|
1707
|
+
host.trace(requestId, "toolchainLoad", "start");
|
|
1708
|
+
const pkg = await Wasmer.fromFile(packageBytes, host.runtime);
|
|
1709
|
+
const compiler = requireCommand(pkg, pins.command);
|
|
1710
|
+
const linker = requireCommand(pkg, pins.linkerCommand);
|
|
1711
|
+
host.trace(requestId, "toolchainLoad", "end");
|
|
1712
|
+
return {
|
|
1713
|
+
pkg,
|
|
1714
|
+
pins,
|
|
1715
|
+
compiler,
|
|
1716
|
+
linker
|
|
1717
|
+
};
|
|
1718
|
+
})();
|
|
1719
|
+
try {
|
|
1720
|
+
return await loadedToolchain;
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
loadedToolchain = void 0;
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function requireCommand(pkg, name) {
|
|
1727
|
+
const selected = pkg.commands[name];
|
|
1728
|
+
if (!selected) throw new Error(`The SDK-direct Clang package does not expose '${name}'.`);
|
|
1729
|
+
return selected;
|
|
1730
|
+
}
|
|
1731
|
+
async function loadLibcxxPch(profile, requestId, host) {
|
|
1732
|
+
loadedLibcxxPchManifest ??= host.loadToolchainFile(CLANG_LIBCXX_PCH_MANIFEST_ASSET_PATH).then(decodeLibcxxPchManifest);
|
|
1733
|
+
let pending = loadedLibcxxPch.get(profile);
|
|
1734
|
+
if (!pending) {
|
|
1735
|
+
pending = loadedLibcxxPchManifest.then(async (manifest) => {
|
|
1736
|
+
const asset = manifest.profiles[profile];
|
|
1737
|
+
host.progress(requestId, "loading-toolchain", `Loading admitted libc++ PCH (${profile})`, .25);
|
|
1738
|
+
const bytes = await host.loadToolchainAsset(`/toolchains/${asset.path}`);
|
|
1739
|
+
if (bytes.byteLength !== asset.byteLength || await sha256Hex(bytes) !== asset.sha256) throw new Error(`Pinned libc++ PCH '${profile}' failed decompressed integrity verification.`);
|
|
1740
|
+
return bytes;
|
|
1741
|
+
});
|
|
1742
|
+
loadedLibcxxPch.set(profile, pending);
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
return await pending;
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
loadedLibcxxPch.delete(profile);
|
|
1748
|
+
throw error;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function findPrecompiledHeader(project) {
|
|
1752
|
+
const headers = project.files.map((file) => file.path).filter((path) => path.split("/").at(-1) === "wasm-oj.pch.hpp");
|
|
1753
|
+
if (headers.length > 1) throw new Error(`C++ projects may contain at most one wasm-oj.pch.hpp; received ${headers.join(", ")}.`);
|
|
1754
|
+
return headers[0];
|
|
1755
|
+
}
|
|
1756
|
+
function runPchStage(command, args, directory, host, requestId, outputPath, dependencyPath) {
|
|
1757
|
+
const stability = new MountedOutputStabilityObserver();
|
|
1758
|
+
return runUntilOutputReady(command, args, directory, host, requestId, "projectSpawn", "projectWait", "projectOutputReady", async (capturedStderr) => {
|
|
1759
|
+
const [snapshot, dependency] = await Promise.all([readOptionalFile(directory, outputPath), readOptionalFile(directory, dependencyPath)]);
|
|
1760
|
+
const pch = stability.observe(snapshot?.byteLength ? snapshot : void 0, performance.now());
|
|
1761
|
+
if (pch && dependency?.byteLength && decoder.decode(dependency).endsWith("\n")) return {
|
|
1762
|
+
pch,
|
|
1763
|
+
dependency
|
|
1764
|
+
};
|
|
1765
|
+
return /\d+ errors? generated\.\s*$/.test(capturedStderr) ? {} : void 0;
|
|
1766
|
+
}).then((observed) => ({
|
|
1767
|
+
...observed.value,
|
|
1768
|
+
diagnostics: parseClangDiagnostics(`${observed.stderr}\n${observed.stdout}`),
|
|
1769
|
+
stdout: observed.stdout,
|
|
1770
|
+
stderr: observed.stderr
|
|
1771
|
+
}));
|
|
1772
|
+
}
|
|
1773
|
+
function runClangStage(command, args, directory, host, requestId, spawnOperation, waitOperation, outputReadyOperation, outputPath, dependencyPath) {
|
|
1774
|
+
return runUntilOutputReady(command, args, directory, host, requestId, spawnOperation, waitOperation, outputReadyOperation, async (capturedStderr) => {
|
|
1775
|
+
const [object, dependency] = await Promise.all([readValidWasmFile(directory, outputPath), readOptionalFile(directory, dependencyPath)]);
|
|
1776
|
+
if (object && dependency?.byteLength && decoder.decode(dependency).endsWith("\n")) return {
|
|
1777
|
+
object,
|
|
1778
|
+
dependency
|
|
1779
|
+
};
|
|
1780
|
+
return /\d+ errors? generated\.\s*$/.test(capturedStderr) ? {} : void 0;
|
|
1781
|
+
}).then((observed) => ({
|
|
1782
|
+
...observed.value,
|
|
1783
|
+
diagnostics: parseClangDiagnostics(`${observed.stderr}\n${observed.stdout}`),
|
|
1784
|
+
stdout: observed.stdout,
|
|
1785
|
+
stderr: observed.stderr
|
|
1786
|
+
}));
|
|
1787
|
+
}
|
|
1788
|
+
function runLinkStage(command, args, directory, host, requestId, spawnOperation, waitOperation, outputReadyOperation, outputPath) {
|
|
1789
|
+
return runUntilOutputReady(command, args, directory, host, requestId, spawnOperation, waitOperation, outputReadyOperation, async (capturedStderr) => {
|
|
1790
|
+
const output = await readValidWasmFile(directory, outputPath);
|
|
1791
|
+
if (output) return output;
|
|
1792
|
+
return /(?:wasm-ld|lld): error:/i.test(capturedStderr) ? null : void 0;
|
|
1793
|
+
}).then((observed) => ({
|
|
1794
|
+
...observed,
|
|
1795
|
+
value: observed.value ?? void 0
|
|
1796
|
+
}));
|
|
1797
|
+
}
|
|
1798
|
+
async function runUntilOutputReady(command, args, directory, host, requestId, spawnOperation, waitOperation, outputReadyOperation, probe) {
|
|
1799
|
+
host.trace(requestId, spawnOperation, "start");
|
|
1800
|
+
const instance = await command.run({
|
|
1801
|
+
args,
|
|
1802
|
+
cwd: "/project",
|
|
1803
|
+
env: {
|
|
1804
|
+
PATH: "/bin",
|
|
1805
|
+
SOURCE_DATE_EPOCH: "946684800",
|
|
1806
|
+
TZ: "UTC",
|
|
1807
|
+
LC_ALL: "C"
|
|
1808
|
+
},
|
|
1809
|
+
mount: { "/project": directory }
|
|
1810
|
+
});
|
|
1811
|
+
host.trace(requestId, spawnOperation, "end");
|
|
1812
|
+
host.trace(requestId, waitOperation, "start");
|
|
1813
|
+
host.trace(requestId, outputReadyOperation, "start");
|
|
1814
|
+
const stdoutCapture = captureReadable(instance.stdout);
|
|
1815
|
+
const stderrCapture = captureReadable(instance.stderr);
|
|
1816
|
+
const deadline = performance.now() + STAGE_OUTPUT_TIMEOUT_MS;
|
|
1817
|
+
try {
|
|
1818
|
+
while (performance.now() < deadline) {
|
|
1819
|
+
const result = await probe(stderrCapture.text());
|
|
1820
|
+
if (result !== void 0) {
|
|
1821
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1822
|
+
return {
|
|
1823
|
+
value: result,
|
|
1824
|
+
stdout: stdoutCapture.text(),
|
|
1825
|
+
stderr: stderrCapture.text()
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1829
|
+
}
|
|
1830
|
+
throw new Error(`Compiler stage did not produce a complete output within ${STAGE_OUTPUT_TIMEOUT_MS} ms.`);
|
|
1831
|
+
} finally {
|
|
1832
|
+
host.trace(requestId, outputReadyOperation, "end");
|
|
1833
|
+
host.trace(requestId, waitOperation, "end");
|
|
1834
|
+
await Promise.all([stdoutCapture.cancel(), stderrCapture.cancel()]);
|
|
1835
|
+
instance.free();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function captureReadable(stream) {
|
|
1839
|
+
const reader = stream.getReader();
|
|
1840
|
+
const chunks = [];
|
|
1841
|
+
(async () => {
|
|
1842
|
+
try {
|
|
1843
|
+
while (true) {
|
|
1844
|
+
const result = await reader.read();
|
|
1845
|
+
if (result.done) return;
|
|
1846
|
+
const bytes = result.value instanceof Uint8Array ? result.value : new Uint8Array(result.value);
|
|
1847
|
+
chunks.push(bytes.slice());
|
|
1848
|
+
}
|
|
1849
|
+
} catch {}
|
|
1850
|
+
})();
|
|
1851
|
+
return {
|
|
1852
|
+
text: () => decoder.decode(concatenate(chunks)),
|
|
1853
|
+
cancel: async () => {
|
|
1854
|
+
try {
|
|
1855
|
+
await reader.cancel();
|
|
1856
|
+
} catch {}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
function concatenate(chunks) {
|
|
1861
|
+
const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.byteLength, 0));
|
|
1862
|
+
let offset = 0;
|
|
1863
|
+
for (const chunk of chunks) {
|
|
1864
|
+
output.set(chunk, offset);
|
|
1865
|
+
offset += chunk.byteLength;
|
|
1866
|
+
}
|
|
1867
|
+
return output;
|
|
1868
|
+
}
|
|
1869
|
+
async function readValidWasmFile(directory, guestPath) {
|
|
1870
|
+
const bytes = await readOptionalFile(directory, guestPath);
|
|
1871
|
+
if (!bytes || bytes.byteLength <= 8) return void 0;
|
|
1872
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
1873
|
+
copy.set(bytes);
|
|
1874
|
+
return WebAssembly.validate(copy.buffer) ? copy : void 0;
|
|
1875
|
+
}
|
|
1876
|
+
async function ensureDirectory(directory, path) {
|
|
1877
|
+
try {
|
|
1878
|
+
await directory.createDir(path);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (!String(error).toLowerCase().includes("exist")) throw error;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
async function readOptionalFile(directory, guestPath) {
|
|
1884
|
+
const mountRelativePath = guestPath.startsWith("/project/") ? guestPath.slice(8) : guestPath;
|
|
1885
|
+
try {
|
|
1886
|
+
return await directory.readFile(mountRelativePath);
|
|
1887
|
+
} catch {
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
function failedBuild(project, stdout, stderr, code, source, providedDiagnostics) {
|
|
1892
|
+
return {
|
|
1893
|
+
success: false,
|
|
1894
|
+
diagnostics: ensureFailureDiagnostic(providedDiagnostics ?? parseClangDiagnostics(`${stderr}\n${stdout}`), {
|
|
1895
|
+
file: project.config.entry,
|
|
1896
|
+
source,
|
|
1897
|
+
message: stderr.trim() || `${source} exited with code ${code}.`
|
|
1898
|
+
}),
|
|
1899
|
+
stdout,
|
|
1900
|
+
stderr,
|
|
1901
|
+
cacheHit: false
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
new TextEncoder();
|
|
1905
|
+
Object.freeze({
|
|
1906
|
+
python: "python",
|
|
1907
|
+
javascript: "qjs",
|
|
1908
|
+
typescript: "qjs"
|
|
1909
|
+
});
|
|
1910
|
+
function requiredTrimmedString(value, label, maximum = 16384) {
|
|
1911
|
+
if (typeof value !== "string" || !value || value !== value.trim() || value.length > maximum) throw new Error(`${label} must be a non-empty, trimmed string of at most ${maximum} characters.`);
|
|
1912
|
+
}
|
|
1913
|
+
function serializeRuntimeBundleManifest(data) {
|
|
1914
|
+
return JSON.stringify({
|
|
1915
|
+
schema: WASM_OJ_SCHEMAS.runtimeBundle,
|
|
1916
|
+
version: WASM_OJ_CONTRACT_VERSION,
|
|
1917
|
+
name: data.name,
|
|
1918
|
+
target: data.target,
|
|
1919
|
+
language: data.language,
|
|
1920
|
+
runtime: {
|
|
1921
|
+
package: data.runtimePackage,
|
|
1922
|
+
command: data.command
|
|
1923
|
+
},
|
|
1924
|
+
execution: {
|
|
1925
|
+
deterministic: true,
|
|
1926
|
+
contractVersion: WASM_OJ_CONTRACT_VERSION
|
|
1927
|
+
},
|
|
1928
|
+
entry: data.entry,
|
|
1929
|
+
files: [...data.files]
|
|
1930
|
+
}, null, 2);
|
|
1931
|
+
}
|
|
1932
|
+
/** Canonical WASM-OJ manifest constructor for built-in and downstream runtime bundles. */
|
|
1933
|
+
function createRuntimeBundleManifest(project, runtimePackage, command, entry) {
|
|
1934
|
+
requiredTrimmedString(project.name, "Project name");
|
|
1935
|
+
requiredTrimmedString(runtimePackage, "Runtime package");
|
|
1936
|
+
requiredTrimmedString(command, "Runtime command", 128);
|
|
1937
|
+
assertLanguageIdentifier(project.config.language);
|
|
1938
|
+
assertSafeRelativePath(entry, "Runtime bundle entry");
|
|
1939
|
+
return serializeRuntimeBundleManifest({
|
|
1940
|
+
name: project.name,
|
|
1941
|
+
target: project.config.target,
|
|
1942
|
+
language: project.config.language,
|
|
1943
|
+
runtimePackage,
|
|
1944
|
+
command,
|
|
1945
|
+
entry,
|
|
1946
|
+
files: canonicalProjectFiles(project.files).map((file) => file.path)
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
/** Produces a new record whose insertion order is canonical and path-safe. */
|
|
1950
|
+
function canonicalRuntimeBundleFiles(files) {
|
|
1951
|
+
return canonicalFileRecord(files);
|
|
1952
|
+
}
|
|
1953
|
+
//#endregion
|
|
1954
|
+
//#region src/core/quickjs-runtime.ts
|
|
1955
|
+
var QUICKJS_STD_MODULE_DECLARATION = String.raw`
|
|
1956
|
+
declare module "std" {
|
|
1957
|
+
const std: {
|
|
1958
|
+
err: { puts(value: string): void };
|
|
1959
|
+
in: { readAsString(): string };
|
|
1960
|
+
out: { puts(value: string): void };
|
|
1961
|
+
};
|
|
1962
|
+
export = std;
|
|
1963
|
+
}
|
|
1964
|
+
`;
|
|
1965
|
+
//#endregion
|
|
1966
|
+
//#region src/compiler/language-driver.ts
|
|
1967
|
+
/** Internal registry for WASM-OJ's built-in compiler pipelines. */
|
|
1968
|
+
var LanguageDriverRegistry = class {
|
|
1969
|
+
drivers = /* @__PURE__ */ new Map();
|
|
1970
|
+
ids = /* @__PURE__ */ new Set();
|
|
1971
|
+
register(driver) {
|
|
1972
|
+
if (!driver || typeof driver !== "object") throw new TypeError("Language drivers must be objects.");
|
|
1973
|
+
if (typeof driver.id !== "string" || !driver.id || driver.id !== driver.id.trim() || driver.id.length > 128) throw new Error("Language driver IDs must be non-empty, trimmed, and at most 128 characters.");
|
|
1974
|
+
if (this.ids.has(driver.id)) throw new Error(`Language driver '${driver.id}' is already registered.`);
|
|
1975
|
+
if (!Array.isArray(driver.languages) || driver.languages.length === 0) throw new Error(`Language driver '${driver.id}' has no languages.`);
|
|
1976
|
+
if (typeof driver.build !== "function") throw new TypeError(`Language driver '${driver.id}' must implement build().`);
|
|
1977
|
+
const languages = /* @__PURE__ */ new Set();
|
|
1978
|
+
for (const language of driver.languages) {
|
|
1979
|
+
if (typeof language !== "string") throw new TypeError("Language identifiers must be strings.");
|
|
1980
|
+
assertLanguageIdentifier(language);
|
|
1981
|
+
if (languages.has(language)) throw new Error(`Language '${language}' is duplicated in driver '${driver.id}'.`);
|
|
1982
|
+
languages.add(language);
|
|
1983
|
+
const existing = this.drivers.get(language);
|
|
1984
|
+
if (existing) throw new Error(`Language '${language}' is already owned by driver '${existing.id}'.`);
|
|
1985
|
+
}
|
|
1986
|
+
for (const language of languages) this.drivers.set(language, driver);
|
|
1987
|
+
this.ids.add(driver.id);
|
|
1988
|
+
}
|
|
1989
|
+
driver(language) {
|
|
1990
|
+
const driver = this.drivers.get(language);
|
|
1991
|
+
if (!driver) throw new Error(`No language driver is registered for '${language}'.`);
|
|
1992
|
+
return driver;
|
|
1993
|
+
}
|
|
1994
|
+
languages() {
|
|
1995
|
+
return [...this.drivers.keys()];
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
//#endregion
|
|
1999
|
+
//#region src/compiler/java-toolchain.ts
|
|
2000
|
+
var JAVA_COMPILE_TIMEOUT_MS = 18e4;
|
|
2001
|
+
Object.freeze({
|
|
2002
|
+
version: JAVA_VERSION,
|
|
2003
|
+
package: JAVA_COMPILER_PACKAGE,
|
|
2004
|
+
compilerAsset: JAVA_COMPILER_ASSET_PATH,
|
|
2005
|
+
compilerCompressedSha256: JAVA_COMPILER_COMPRESSED_PACKAGE_SHA256,
|
|
2006
|
+
compilerPackageSha256: JAVA_COMPILER_PACKAGE_SHA256,
|
|
2007
|
+
compileClasslibAsset: JAVA_COMPILE_CLASSLIB_ASSET_PATH,
|
|
2008
|
+
compileClasslibSha256: JAVA_COMPILE_CLASSLIB_SHA256,
|
|
2009
|
+
runtimeClasslibAsset: JAVA_RUNTIME_CLASSLIB_ASSET_PATH,
|
|
2010
|
+
runtimeClasslibSha256: JAVA_RUNTIME_CLASSLIB_SHA256
|
|
2011
|
+
});
|
|
2012
|
+
function javaMainClass(entry, source) {
|
|
2013
|
+
const className = entry.split("/").at(-1)?.replace(/\.java$/u, "") ?? "";
|
|
2014
|
+
if (!/^[A-Za-z_$][\w$]*$/u.test(className)) throw new Error(`Java entry '${entry}' must name a valid .java class file.`);
|
|
2015
|
+
const packageName = source.match(/^\s*package\s+([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*;/m)?.[1];
|
|
2016
|
+
return packageName ? `${packageName}.${className}` : className;
|
|
2017
|
+
}
|
|
2018
|
+
//#endregion
|
|
2019
|
+
//#region src/compiler/wasmer-engine.ts
|
|
2020
|
+
var encoder = new TextEncoder();
|
|
2021
|
+
var typescriptCompilerBytes;
|
|
2022
|
+
var host;
|
|
2023
|
+
function configureWasmerCompilerHost(nextHost) {
|
|
2024
|
+
host = nextHost;
|
|
2025
|
+
}
|
|
2026
|
+
function progress(requestId, phase, label, value) {
|
|
2027
|
+
requireHost().progress(requestId, phase, label, value);
|
|
2028
|
+
}
|
|
2029
|
+
function requireHost() {
|
|
2030
|
+
if (!host) throw new Error("Wasmer compiler host is not configured.");
|
|
2031
|
+
return host;
|
|
2032
|
+
}
|
|
2033
|
+
function requireRuntime() {
|
|
2034
|
+
return requireHost().getRuntime();
|
|
2035
|
+
}
|
|
2036
|
+
async function getTypeScriptCompiler() {
|
|
2037
|
+
typescriptCompilerBytes ??= requireHost().loadToolchainAsset(TYPESCRIPT_ASSET_PATH);
|
|
2038
|
+
try {
|
|
2039
|
+
return Wasmer.fromWasm(await typescriptCompilerBytes, requireRuntime());
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
typescriptCompilerBytes = void 0;
|
|
2042
|
+
throw error;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function createArtifactBase(project, cacheKey, started, size, toolchains, contentIdentity) {
|
|
2046
|
+
return {
|
|
2047
|
+
wasmOjContract: WASM_OJ_CONTRACT_VERSION,
|
|
2048
|
+
id: crypto.randomUUID(),
|
|
2049
|
+
projectId: project.id,
|
|
2050
|
+
cacheKey,
|
|
2051
|
+
name: `${project.name}.${project.config.target === "wasip1" ? "wasm" : "wasix.wasm"}`,
|
|
2052
|
+
language: project.config.language,
|
|
2053
|
+
target: project.config.target,
|
|
2054
|
+
optimization: project.config.optimization,
|
|
2055
|
+
createdAt: Date.now(),
|
|
2056
|
+
durationMs: performance.now() - started,
|
|
2057
|
+
size,
|
|
2058
|
+
toolchains,
|
|
2059
|
+
costProfile: costProfileId(project.config.language, project.config.target, project.config.optimization, contentIdentity),
|
|
2060
|
+
...project.dependencies === void 0 ? {} : { dependencyLockSha256: project.dependencies.lockSha256 }
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
async function buildRust(project, cacheKey, requestId) {
|
|
2064
|
+
const started = performance.now();
|
|
2065
|
+
progress(requestId, "compiling", `Compiling with rustc ${RUST_VERSION}`, .2);
|
|
2066
|
+
const dependencies = rustDependencyInput(project);
|
|
2067
|
+
const compiled = await requireHost().compileRust({
|
|
2068
|
+
entry: project.config.entry,
|
|
2069
|
+
files: [...project.files, ...dependencies.files],
|
|
2070
|
+
optimization: project.config.optimization,
|
|
2071
|
+
dependencies: dependencies.crates,
|
|
2072
|
+
rootExterns: dependencies.roots
|
|
2073
|
+
});
|
|
2074
|
+
if (!compiled.success || !compiled.wasm) return {
|
|
2075
|
+
success: false,
|
|
2076
|
+
diagnostics: ensureFailureDiagnostic(compiled.diagnostics, {
|
|
2077
|
+
file: project.config.entry,
|
|
2078
|
+
source: "rustc",
|
|
2079
|
+
message: compiled.stderr.trim() || "rustc failed without a diagnostic."
|
|
2080
|
+
}),
|
|
2081
|
+
stdout: compiled.stdout,
|
|
2082
|
+
stderr: compiled.stderr,
|
|
2083
|
+
cacheHit: false
|
|
2084
|
+
};
|
|
2085
|
+
const bytes = compiled.wasm;
|
|
2086
|
+
const artifact = {
|
|
2087
|
+
kind: "wasm",
|
|
2088
|
+
...createArtifactBase(project, cacheKey, started, bytes.byteLength, toolchainPackageIdentities("rust")),
|
|
2089
|
+
bytes
|
|
2090
|
+
};
|
|
2091
|
+
return {
|
|
2092
|
+
success: true,
|
|
2093
|
+
diagnostics: compiled.diagnostics,
|
|
2094
|
+
artifact,
|
|
2095
|
+
stdout: compiled.stdout,
|
|
2096
|
+
stderr: compiled.stderr,
|
|
2097
|
+
cacheHit: false
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function sumFileSize(files) {
|
|
2101
|
+
return Object.values(files).reduce((total, file) => total + (typeof file === "string" ? encoder.encode(file).byteLength : file.byteLength), 0);
|
|
2102
|
+
}
|
|
2103
|
+
async function buildPython(project, cacheKey, requestId) {
|
|
2104
|
+
const started = performance.now();
|
|
2105
|
+
const dependencies = pythonDependencyFiles(project);
|
|
2106
|
+
const compilerFiles = [...project.files, ...dependencies.sourceFiles];
|
|
2107
|
+
const pythonFiles = compilerFiles.filter((file) => file.path.endsWith(".py"));
|
|
2108
|
+
progress(requestId, "compiling", `Byte-compiling ${pythonFiles.length} Python file${pythonFiles.length === 1 ? "" : "s"}`, .55);
|
|
2109
|
+
const frontend = await requireHost().compilePython({ files: compilerFiles });
|
|
2110
|
+
if (!frontend.success) return {
|
|
2111
|
+
success: false,
|
|
2112
|
+
diagnostics: ensureFailureDiagnostic(frontend.diagnostics, {
|
|
2113
|
+
file: project.config.entry,
|
|
2114
|
+
source: "python",
|
|
2115
|
+
message: frontend.stderr.trim() || "Python byte-compilation failed without a diagnostic."
|
|
2116
|
+
}),
|
|
2117
|
+
stdout: frontend.stdout,
|
|
2118
|
+
stderr: frontend.stderr,
|
|
2119
|
+
cacheHit: false
|
|
2120
|
+
};
|
|
2121
|
+
const files = Object.fromEntries(project.files.map((file) => [file.path, file.content]));
|
|
2122
|
+
Object.assign(files, dependencies.artifactFiles);
|
|
2123
|
+
files[PYTHON_RUNNER_PATH] = PYTHON_DETERMINISTIC_RUNNER;
|
|
2124
|
+
for (const file of pythonFiles) {
|
|
2125
|
+
const compiledPath = `build/${file.path.replace(/\.py$/, ".pyc")}`;
|
|
2126
|
+
const bytecode = frontend.bytecode[compiledPath];
|
|
2127
|
+
if (!bytecode) throw new Error(`Python stage omitted '${compiledPath}'.`);
|
|
2128
|
+
files[compiledPath] = bytecode;
|
|
2129
|
+
}
|
|
2130
|
+
const entry = `build/${project.config.entry.replace(/\.py$/, ".pyc")}`;
|
|
2131
|
+
const manifest = createRuntimeBundleManifest(project, PYTHON_PACKAGE, "python", entry);
|
|
2132
|
+
files["wasm-oj.manifest.json"] = manifest;
|
|
2133
|
+
const bundleFiles = canonicalRuntimeBundleFiles(files);
|
|
2134
|
+
const artifact = {
|
|
2135
|
+
kind: "runtime-bundle",
|
|
2136
|
+
...createArtifactBase(project, cacheKey, started, sumFileSize(bundleFiles), toolchainPackageIdentities("python")),
|
|
2137
|
+
name: `${project.name}.python-${project.config.target}.json`,
|
|
2138
|
+
runtimePackage: PYTHON_PACKAGE,
|
|
2139
|
+
command: "python",
|
|
2140
|
+
entry,
|
|
2141
|
+
files: bundleFiles,
|
|
2142
|
+
manifest
|
|
2143
|
+
};
|
|
2144
|
+
return {
|
|
2145
|
+
success: true,
|
|
2146
|
+
diagnostics: frontend.diagnostics,
|
|
2147
|
+
artifact,
|
|
2148
|
+
stdout: frontend.stdout,
|
|
2149
|
+
stderr: frontend.stderr,
|
|
2150
|
+
cacheHit: false
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
async function buildGo(project, cacheKey, requestId) {
|
|
2154
|
+
const started = performance.now();
|
|
2155
|
+
progress(requestId, "compiling", `Compiling with Go ${GO_VERSION}`, .3);
|
|
2156
|
+
const dependencies = goDependencyInput(project);
|
|
2157
|
+
const compiled = await requireHost().compileGo({
|
|
2158
|
+
entry: project.config.entry,
|
|
2159
|
+
files: project.files,
|
|
2160
|
+
dependencyFiles: dependencies.files,
|
|
2161
|
+
optimization: project.config.optimization,
|
|
2162
|
+
dependencies: dependencies.packages
|
|
2163
|
+
});
|
|
2164
|
+
if (!compiled.success || !compiled.wasm) return {
|
|
2165
|
+
success: false,
|
|
2166
|
+
diagnostics: ensureFailureDiagnostic(compiled.diagnostics, {
|
|
2167
|
+
file: project.config.entry,
|
|
2168
|
+
source: "go",
|
|
2169
|
+
message: compiled.stderr.trim() || "Go compilation failed without a diagnostic."
|
|
2170
|
+
}),
|
|
2171
|
+
stdout: compiled.stdout,
|
|
2172
|
+
stderr: compiled.stderr,
|
|
2173
|
+
cacheHit: false
|
|
2174
|
+
};
|
|
2175
|
+
const artifact = {
|
|
2176
|
+
kind: "wasm",
|
|
2177
|
+
...createArtifactBase(project, cacheKey, started, compiled.wasm.byteLength, toolchainPackageIdentities("go")),
|
|
2178
|
+
bytes: compiled.wasm
|
|
2179
|
+
};
|
|
2180
|
+
return {
|
|
2181
|
+
success: true,
|
|
2182
|
+
diagnostics: compiled.diagnostics,
|
|
2183
|
+
artifact,
|
|
2184
|
+
stdout: compiled.stdout,
|
|
2185
|
+
stderr: compiled.stderr,
|
|
2186
|
+
cacheHit: false
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
async function buildJava(project, cacheKey, requestId) {
|
|
2190
|
+
const started = performance.now();
|
|
2191
|
+
const entry = project.files.find((file) => file.path === project.config.entry);
|
|
2192
|
+
if (!entry || !entry.path.endsWith(".java")) return {
|
|
2193
|
+
success: false,
|
|
2194
|
+
diagnostics: [{
|
|
2195
|
+
severity: "error",
|
|
2196
|
+
message: "The Java entry must be a .java source file.",
|
|
2197
|
+
file: project.config.entry,
|
|
2198
|
+
line: 1,
|
|
2199
|
+
column: 1,
|
|
2200
|
+
source: "project"
|
|
2201
|
+
}],
|
|
2202
|
+
stdout: "",
|
|
2203
|
+
stderr: "",
|
|
2204
|
+
cacheHit: false
|
|
2205
|
+
};
|
|
2206
|
+
progress(requestId, "compiling", `Compiling Java ${javaMainClass(entry.path, entry.content)}`, .3);
|
|
2207
|
+
const compiled = await requireHost().compileJava({
|
|
2208
|
+
entry: project.config.entry,
|
|
2209
|
+
files: project.files,
|
|
2210
|
+
optimization: project.config.optimization
|
|
2211
|
+
});
|
|
2212
|
+
if (!compiled.success || !compiled.wasm) return {
|
|
2213
|
+
success: false,
|
|
2214
|
+
diagnostics: ensureFailureDiagnostic(compiled.diagnostics, {
|
|
2215
|
+
file: project.config.entry,
|
|
2216
|
+
source: "java",
|
|
2217
|
+
message: compiled.stderr.trim() || "Java compilation failed without a diagnostic."
|
|
2218
|
+
}),
|
|
2219
|
+
stdout: compiled.stdout,
|
|
2220
|
+
stderr: compiled.stderr,
|
|
2221
|
+
cacheHit: false
|
|
2222
|
+
};
|
|
2223
|
+
const artifact = {
|
|
2224
|
+
kind: "wasm",
|
|
2225
|
+
...createArtifactBase(project, cacheKey, started, compiled.wasm.byteLength, toolchainPackageIdentities("java"), toolchainContentIdentity("java")),
|
|
2226
|
+
bytes: compiled.wasm
|
|
2227
|
+
};
|
|
2228
|
+
return {
|
|
2229
|
+
success: true,
|
|
2230
|
+
diagnostics: compiled.diagnostics,
|
|
2231
|
+
artifact,
|
|
2232
|
+
stdout: compiled.stdout,
|
|
2233
|
+
stderr: compiled.stderr,
|
|
2234
|
+
cacheHit: false
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
function emittedScriptPath(path) {
|
|
2238
|
+
if (path.endsWith(".ts")) return path.slice(0, -3) + ".js";
|
|
2239
|
+
return path;
|
|
2240
|
+
}
|
|
2241
|
+
function scriptSourceFiles(project) {
|
|
2242
|
+
const extension = project.config.language === "typescript" ? ".ts" : ".js";
|
|
2243
|
+
return project.files.filter((file) => file.path.endsWith(extension));
|
|
2244
|
+
}
|
|
2245
|
+
function emittedSourceFiles(project) {
|
|
2246
|
+
return scriptSourceFiles(project).filter((file) => !file.path.endsWith(".d.ts"));
|
|
2247
|
+
}
|
|
2248
|
+
async function transpileScriptProject(project, requestId) {
|
|
2249
|
+
const scriptFiles = scriptSourceFiles(project);
|
|
2250
|
+
const emittedFiles = emittedSourceFiles(project);
|
|
2251
|
+
const dependencyFiles = npmDependencyFiles(project);
|
|
2252
|
+
progress(requestId, "loading-toolchain", `Loading TypeScript ${TYPESCRIPT_VERSION}/WASI`);
|
|
2253
|
+
const entrypoint = (await getTypeScriptCompiler()).entrypoint;
|
|
2254
|
+
if (!entrypoint) throw new Error("The TypeScript/WASI compiler has no executable entrypoint.");
|
|
2255
|
+
const outputPaths = emittedFiles.map((file) => emittedScriptPath(file.path));
|
|
2256
|
+
const declarationPath = "/project/.wasm-oj/quickjs.d.ts";
|
|
2257
|
+
const output = await (await entrypoint.run({ stdin: JSON.stringify({
|
|
2258
|
+
files: {
|
|
2259
|
+
...Object.fromEntries(project.files.map((file) => [`/project/${file.path}`, file.content])),
|
|
2260
|
+
...Object.fromEntries(Object.entries(dependencyFiles).filter(([, contents]) => typeof contents === "string").map(([path, contents]) => [`/project/${path}`, contents])),
|
|
2261
|
+
[declarationPath]: QUICKJS_STD_MODULE_DECLARATION
|
|
2262
|
+
},
|
|
2263
|
+
javascript: project.config.language === "javascript",
|
|
2264
|
+
sources: [
|
|
2265
|
+
declarationPath,
|
|
2266
|
+
...scriptFiles.map((file) => `/project/${file.path}`),
|
|
2267
|
+
...Object.entries(dependencyFiles).filter(([path, contents]) => path.endsWith(".d.ts") && typeof contents === "string").map(([path]) => `/project/${path}`)
|
|
2268
|
+
],
|
|
2269
|
+
outputs: outputPaths.map((path) => `/project/build/${path}`)
|
|
2270
|
+
}) })).wait();
|
|
2271
|
+
let response;
|
|
2272
|
+
if (output.ok) try {
|
|
2273
|
+
response = JSON.parse(output.stdout);
|
|
2274
|
+
} catch {
|
|
2275
|
+
response = void 0;
|
|
2276
|
+
}
|
|
2277
|
+
const files = {};
|
|
2278
|
+
if (response) for (const outputPath of outputPaths) {
|
|
2279
|
+
const contents = response.files[`/project/build/${outputPath}`];
|
|
2280
|
+
if (contents !== void 0) files[outputPath] = contents;
|
|
2281
|
+
}
|
|
2282
|
+
Object.assign(files, dependencyFiles);
|
|
2283
|
+
return {
|
|
2284
|
+
files,
|
|
2285
|
+
output,
|
|
2286
|
+
response
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
async function buildScript(project, cacheKey, requestId) {
|
|
2290
|
+
const started = performance.now();
|
|
2291
|
+
if (!emittedSourceFiles(project).some((file) => file.path === project.config.entry)) return {
|
|
2292
|
+
success: false,
|
|
2293
|
+
diagnostics: [{
|
|
2294
|
+
severity: "error",
|
|
2295
|
+
message: `The ${project.config.language === "typescript" ? ".ts" : ".js"} entry file is not a supported executable source.`,
|
|
2296
|
+
file: project.config.entry,
|
|
2297
|
+
line: 1,
|
|
2298
|
+
column: 1,
|
|
2299
|
+
source: "project"
|
|
2300
|
+
}],
|
|
2301
|
+
stdout: "",
|
|
2302
|
+
stderr: "",
|
|
2303
|
+
cacheHit: false
|
|
2304
|
+
};
|
|
2305
|
+
progress(requestId, "compiling", `Compiling ${project.config.language === "typescript" ? "TypeScript" : "JavaScript"} with TypeScript/WASI`, .5);
|
|
2306
|
+
const { files, output, response } = await transpileScriptProject(project, requestId);
|
|
2307
|
+
const diagnostics = parseTypeScriptDiagnostics(response?.diagnostics ?? "");
|
|
2308
|
+
const emittedOutputsPresent = emittedSourceFiles(project).every((file) => Object.hasOwn(files, emittedScriptPath(file.path)));
|
|
2309
|
+
if (!output.ok || !response || response.status !== 0 || !emittedOutputsPresent || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
|
|
2310
|
+
success: false,
|
|
2311
|
+
diagnostics: ensureFailureDiagnostic(diagnostics, {
|
|
2312
|
+
file: project.config.entry,
|
|
2313
|
+
source: "typescript",
|
|
2314
|
+
message: output.stderr.trim() || response?.diagnostics.trim() || `TypeScript 7.0.2 did not return every compiled output.`
|
|
2315
|
+
}),
|
|
2316
|
+
stdout: "",
|
|
2317
|
+
stderr: output.stderr,
|
|
2318
|
+
cacheHit: false
|
|
2319
|
+
};
|
|
2320
|
+
const entry = emittedScriptPath(project.config.entry);
|
|
2321
|
+
const manifest = createRuntimeBundleManifest(project, QUICKJS_PACKAGE, "qjs", entry);
|
|
2322
|
+
files["wasm-oj.manifest.json"] = manifest;
|
|
2323
|
+
const bundleFiles = canonicalRuntimeBundleFiles(files);
|
|
2324
|
+
return {
|
|
2325
|
+
success: true,
|
|
2326
|
+
diagnostics,
|
|
2327
|
+
artifact: {
|
|
2328
|
+
kind: "runtime-bundle",
|
|
2329
|
+
...createArtifactBase(project, cacheKey, started, sumFileSize(bundleFiles), toolchainPackageIdentities(project.config.language)),
|
|
2330
|
+
name: `${project.name}.${project.config.language === "typescript" ? "typescript" : "javascript"}-${project.config.target}.json`,
|
|
2331
|
+
runtimePackage: QUICKJS_PACKAGE,
|
|
2332
|
+
command: "qjs",
|
|
2333
|
+
entry,
|
|
2334
|
+
files: bundleFiles,
|
|
2335
|
+
manifest
|
|
2336
|
+
},
|
|
2337
|
+
stdout: "",
|
|
2338
|
+
stderr: output.stderr,
|
|
2339
|
+
cacheHit: false
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
async function buildProject(project, cacheKey, requestId) {
|
|
2343
|
+
const canonicalProject = {
|
|
2344
|
+
...project,
|
|
2345
|
+
files: canonicalProjectFiles(project.files)
|
|
2346
|
+
};
|
|
2347
|
+
assertProjectDependencyEcosystem(canonicalProject);
|
|
2348
|
+
progress(requestId, "checking", "Validating project configuration", .05);
|
|
2349
|
+
if (!canonicalProject.files.some((file) => file.path === canonicalProject.config.entry)) return {
|
|
2350
|
+
success: false,
|
|
2351
|
+
diagnostics: [{
|
|
2352
|
+
severity: "error",
|
|
2353
|
+
message: "Configured entry file does not exist.",
|
|
2354
|
+
file: canonicalProject.config.entry,
|
|
2355
|
+
line: 1,
|
|
2356
|
+
column: 1,
|
|
2357
|
+
source: "project"
|
|
2358
|
+
}],
|
|
2359
|
+
stdout: "",
|
|
2360
|
+
stderr: "",
|
|
2361
|
+
cacheHit: false
|
|
2362
|
+
};
|
|
2363
|
+
if (canonicalProject.config.language === "c" || canonicalProject.config.language === "cpp") {
|
|
2364
|
+
const activeHost = requireHost();
|
|
2365
|
+
return buildClangWithSdkDirect(canonicalProject, cacheKey, requestId, {
|
|
2366
|
+
runtime: activeHost.getRuntime(),
|
|
2367
|
+
loadToolchainAsset: activeHost.loadToolchainAsset,
|
|
2368
|
+
loadToolchainFile: activeHost.loadToolchainFile,
|
|
2369
|
+
progress: activeHost.progress,
|
|
2370
|
+
trace: activeHost.trace
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
return languageDrivers.driver(canonicalProject.config.language).build({
|
|
2374
|
+
project: canonicalProject,
|
|
2375
|
+
cacheKey,
|
|
2376
|
+
requestId
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
var languageDrivers = new LanguageDriverRegistry();
|
|
2380
|
+
languageDrivers.register({
|
|
2381
|
+
id: "rustc",
|
|
2382
|
+
languages: ["rust"],
|
|
2383
|
+
build: ({ project, cacheKey, requestId }) => buildRust(project, cacheKey, requestId)
|
|
2384
|
+
});
|
|
2385
|
+
languageDrivers.register({
|
|
2386
|
+
id: "cpython",
|
|
2387
|
+
languages: ["python"],
|
|
2388
|
+
build: ({ project, cacheKey, requestId }) => buildPython(project, cacheKey, requestId)
|
|
2389
|
+
});
|
|
2390
|
+
languageDrivers.register({
|
|
2391
|
+
id: "typescript",
|
|
2392
|
+
languages: ["javascript", "typescript"],
|
|
2393
|
+
build: ({ project, cacheKey, requestId }) => buildScript(project, cacheKey, requestId)
|
|
2394
|
+
});
|
|
2395
|
+
languageDrivers.register({
|
|
2396
|
+
id: "go",
|
|
2397
|
+
languages: ["go"],
|
|
2398
|
+
build: ({ project, cacheKey, requestId }) => buildGo(project, cacheKey, requestId)
|
|
2399
|
+
});
|
|
2400
|
+
languageDrivers.register({
|
|
2401
|
+
id: "teavm-java",
|
|
2402
|
+
languages: ["java"],
|
|
2403
|
+
build: ({ project, cacheKey, requestId }) => buildJava(project, cacheKey, requestId)
|
|
2404
|
+
});
|
|
2405
|
+
function clearCompilerHostCaches() {
|
|
2406
|
+
typescriptCompilerBytes = void 0;
|
|
2407
|
+
}
|
|
2408
|
+
//#endregion
|
|
2409
|
+
//#region src/compiler/python-toolchain.ts
|
|
2410
|
+
var PYTHON_COMPILE_TIMEOUT_MS = 18e4;
|
|
2411
|
+
//#endregion
|
|
2412
|
+
//#region src/compiler/rust-toolchain.ts
|
|
2413
|
+
var RUST_TARGET_TRIPLE = "wasm32-wasip1-threads";
|
|
2414
|
+
var RUST_EDITION = "2024";
|
|
2415
|
+
var RUST_COMPILE_TIMEOUT_MS = 18e4;
|
|
2416
|
+
Object.freeze(["wasi_snapshot_preview1.random_get", "wasi_snapshot_preview1.clock_time_get"]);
|
|
2417
|
+
var RUST_TOOLCHAIN = Object.freeze({
|
|
2418
|
+
version: RUST_VERSION,
|
|
2419
|
+
packageAsset: RUST_PACKAGE_ASSET_PATH,
|
|
2420
|
+
packageCompressedSha256: RUST_COMPRESSED_PACKAGE_SHA256,
|
|
2421
|
+
packageSha256: RUST_PACKAGE_SHA256,
|
|
2422
|
+
manifestAsset: RUST_PACKAGE_MANIFEST_ASSET_PATH,
|
|
2423
|
+
manifestSha256: RUST_PACKAGE_MANIFEST_SHA256,
|
|
2424
|
+
target: RUST_TARGET_TRIPLE,
|
|
2425
|
+
edition: RUST_EDITION
|
|
2426
|
+
});
|
|
2427
|
+
`${GO_VERSION.split(".").slice(0, 2).join(".")}`;
|
|
2428
|
+
var GO_COMPILE_TIMEOUT_MS = 18e4;
|
|
2429
|
+
var GO_TOOLCHAIN = Object.freeze({
|
|
2430
|
+
version: GO_VERSION,
|
|
2431
|
+
target: "wasip1/wasm",
|
|
2432
|
+
packageAsset: GO_PACKAGE_ASSET_PATH,
|
|
2433
|
+
packageCompressedSha256: GO_COMPRESSED_PACKAGE_SHA256,
|
|
2434
|
+
packageSha256: GO_PACKAGE_SHA256,
|
|
2435
|
+
manifestAsset: GO_PACKAGE_MANIFEST_ASSET_PATH,
|
|
2436
|
+
manifestSha256: GO_PACKAGE_MANIFEST_SHA256,
|
|
2437
|
+
standardLibraryAsset: GO_STANDARD_LIBRARY_ASSET_PATH,
|
|
2438
|
+
standardLibraryCompressedSha256: GO_COMPRESSED_STANDARD_LIBRARY_SHA256,
|
|
2439
|
+
standardLibrarySha256: GO_STANDARD_LIBRARY_SHA256,
|
|
2440
|
+
compilerSha256: GO_COMPILER_SHA256,
|
|
2441
|
+
linkerSha256: GO_LINKER_SHA256
|
|
2442
|
+
});
|
|
2443
|
+
//#endregion
|
|
2444
|
+
//#region src/server/wasmer-runtime.ts
|
|
2445
|
+
var initialization;
|
|
2446
|
+
async function initializeServerWasmerSdk() {
|
|
2447
|
+
initialization ??= init({ log: "error" }).then(() => void 0);
|
|
2448
|
+
try {
|
|
2449
|
+
await initialization;
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
initialization = void 0;
|
|
2452
|
+
throw error;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
//#endregion
|
|
2456
|
+
//#region src/server/bounded-transport.ts
|
|
2457
|
+
/** Collects a child-process channel without permitting unbounded host memory growth. */
|
|
2458
|
+
var BoundedByteCollector = class {
|
|
2459
|
+
chunks = [];
|
|
2460
|
+
label;
|
|
2461
|
+
maximumBytes;
|
|
2462
|
+
onLimitExceeded;
|
|
2463
|
+
totalBytes = 0;
|
|
2464
|
+
limitError;
|
|
2465
|
+
constructor(label, maximumBytes, onLimitExceeded) {
|
|
2466
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) throw new TypeError("A bounded transport limit must be a positive safe integer.");
|
|
2467
|
+
this.label = label;
|
|
2468
|
+
this.maximumBytes = maximumBytes;
|
|
2469
|
+
this.onLimitExceeded = onLimitExceeded;
|
|
2470
|
+
}
|
|
2471
|
+
append(chunk) {
|
|
2472
|
+
if (this.limitError) return;
|
|
2473
|
+
this.totalBytes += chunk.byteLength;
|
|
2474
|
+
if (this.totalBytes > this.maximumBytes) {
|
|
2475
|
+
this.limitError = /* @__PURE__ */ new Error(`${this.label} exceeded the ${this.maximumBytes} byte transport boundary.`);
|
|
2476
|
+
this.chunks.length = 0;
|
|
2477
|
+
this.onLimitExceeded(this.limitError);
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
this.chunks.push(Buffer.from(chunk));
|
|
2481
|
+
}
|
|
2482
|
+
bytes() {
|
|
2483
|
+
if (this.limitError) throw this.limitError;
|
|
2484
|
+
return Buffer.concat(this.chunks, this.totalBytes);
|
|
2485
|
+
}
|
|
2486
|
+
text() {
|
|
2487
|
+
return this.bytes().toString();
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
/** Reads a private one-shot response through a stable descriptor and enforces a hard byte cap. */
|
|
2491
|
+
async function readBoundedRegularFile(filename, maximumBytes) {
|
|
2492
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) throw new TypeError("A bounded file limit must be a positive safe integer.");
|
|
2493
|
+
const pathStatus = await lstat(filename);
|
|
2494
|
+
if (!pathStatus.isFile()) throw new Error(`Transport response '${filename}' must be a regular file.`);
|
|
2495
|
+
if (pathStatus.size > maximumBytes) throw new Error(`Transport response '${filename}' exceeds the ${maximumBytes} byte boundary.`);
|
|
2496
|
+
const handle = await open(filename, "r");
|
|
2497
|
+
try {
|
|
2498
|
+
const descriptorStatus = await handle.stat();
|
|
2499
|
+
if (!descriptorStatus.isFile() || descriptorStatus.dev !== pathStatus.dev || descriptorStatus.ino !== pathStatus.ino) throw new Error(`Transport response '${filename}' changed before it could be read.`);
|
|
2500
|
+
const chunks = [];
|
|
2501
|
+
let totalBytes = 0;
|
|
2502
|
+
while (true) {
|
|
2503
|
+
const chunk = Buffer.allocUnsafe(Math.min(65536, maximumBytes - totalBytes + 1));
|
|
2504
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, null);
|
|
2505
|
+
if (bytesRead === 0) break;
|
|
2506
|
+
totalBytes += bytesRead;
|
|
2507
|
+
if (totalBytes > maximumBytes) throw new Error(`Transport response '${filename}' exceeds the ${maximumBytes} byte boundary.`);
|
|
2508
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
2509
|
+
}
|
|
2510
|
+
return Buffer.concat(chunks, totalBytes);
|
|
2511
|
+
} finally {
|
|
2512
|
+
await handle.close();
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
//#endregion
|
|
2516
|
+
//#region src/compiler/build-timeout-policy.ts
|
|
2517
|
+
var CLANG_BUILD_CONTROL_TIMEOUT_MS = 6e4;
|
|
2518
|
+
var DEFAULT_BUILD_CONTROL_TIMEOUT_MS = 12e4;
|
|
2519
|
+
var GO_BUILD_CONTROL_TIMEOUT_MS = GO_COMPILE_TIMEOUT_MS + 1e4;
|
|
2520
|
+
var RUST_BUILD_CONTROL_TIMEOUT_MS = RUST_COMPILE_TIMEOUT_MS + 1e4;
|
|
2521
|
+
/**
|
|
2522
|
+
* Hard host deadline for one complete compiler request. The server child and
|
|
2523
|
+
* browser Worker must use the same policy because an SDK call can block the
|
|
2524
|
+
* JavaScript event loop and therefore cannot enforce its own timer.
|
|
2525
|
+
*/
|
|
2526
|
+
function buildControlTimeoutMs(language) {
|
|
2527
|
+
if (language === "java") return 19e4;
|
|
2528
|
+
if (!isBuiltinLanguage(language)) throw new Error(`The built-in WASM-OJ compiler does not support language '${language}'.`);
|
|
2529
|
+
if (language === "c" || language === "cpp") return CLANG_BUILD_CONTROL_TIMEOUT_MS;
|
|
2530
|
+
if (language === "rust") return RUST_BUILD_CONTROL_TIMEOUT_MS;
|
|
2531
|
+
if (language === "go") return GO_BUILD_CONTROL_TIMEOUT_MS;
|
|
2532
|
+
return DEFAULT_BUILD_CONTROL_TIMEOUT_MS;
|
|
2533
|
+
}
|
|
2534
|
+
//#endregion
|
|
2535
|
+
//#region src/server/toolchain-sources.ts
|
|
2536
|
+
function snapshotServerToolchainSources(sources) {
|
|
2537
|
+
validateServerToolchainSources(sources);
|
|
2538
|
+
return Object.freeze(sources.map((source) => Object.freeze({
|
|
2539
|
+
kind: "server",
|
|
2540
|
+
descriptor: freezeDescriptor(source.descriptor),
|
|
2541
|
+
directory: new URL(source.directory.href)
|
|
2542
|
+
})));
|
|
2543
|
+
}
|
|
2544
|
+
function serializeServerToolchainSources(sources) {
|
|
2545
|
+
return Object.freeze(sources.map((source) => Object.freeze({
|
|
2546
|
+
kind: "server",
|
|
2547
|
+
descriptor: source.descriptor,
|
|
2548
|
+
directory: source.directory.href
|
|
2549
|
+
})));
|
|
2550
|
+
}
|
|
2551
|
+
function serverToolchainAssetFile(sources, assetPath) {
|
|
2552
|
+
const { source, asset } = toolchainAssetSource(sources, assetPath);
|
|
2553
|
+
const filename = path.basename(asset.path);
|
|
2554
|
+
const file = fileURLToPath(new URL(filename, source.directory));
|
|
2555
|
+
const directory = fileURLToPath(source.directory);
|
|
2556
|
+
const relative = path.relative(directory, file);
|
|
2557
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Toolchain asset escapes its package directory: '${assetPath}'.`);
|
|
2558
|
+
return file;
|
|
2559
|
+
}
|
|
2560
|
+
function serverToolchainAssetFiles(sources, assetPaths) {
|
|
2561
|
+
return Object.freeze(Object.fromEntries(assetPaths.map((assetPath) => [assetPath, serverToolchainAssetFile(sources, assetPath)])));
|
|
2562
|
+
}
|
|
2563
|
+
function serverToolchainDirectories(sources) {
|
|
2564
|
+
return Object.freeze([...new Set(sources.map((source) => fileURLToPath(source.directory)))]);
|
|
2565
|
+
}
|
|
2566
|
+
function assertServerToolchainProfile(sources, language, target, optimization) {
|
|
2567
|
+
toolchainProfileSource(sources, language, target, optimization);
|
|
2568
|
+
}
|
|
2569
|
+
function freezeDescriptor(descriptor) {
|
|
2570
|
+
return Object.freeze({
|
|
2571
|
+
...descriptor,
|
|
2572
|
+
languages: Object.freeze([...descriptor.languages]),
|
|
2573
|
+
profiles: Object.freeze(descriptor.profiles.map((profile) => Object.freeze({ ...profile }))),
|
|
2574
|
+
assets: Object.freeze(descriptor.assets.map((asset) => Object.freeze({ ...asset })))
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
//#endregion
|
|
2578
|
+
//#region src/server/verified-distribution.ts
|
|
2579
|
+
var SHA256$1 = /^[0-9a-f]{64}$/u;
|
|
2580
|
+
var verifiedDistributions = /* @__PURE__ */ new WeakSet();
|
|
2581
|
+
/** @internal Create a process-local capability from already verified container inventory. */
|
|
2582
|
+
function createVerifiedServerDistribution(evidence) {
|
|
2583
|
+
if (!SHA256$1.test(evidence.compilerSha256) || !SHA256$1.test(evidence.runnerSha256) || !SHA256$1.test(evidence.toolchainRootSha256)) throw new Error("Verified WASM-OJ distribution evidence contains an invalid digest.");
|
|
2584
|
+
const toolchains = snapshotServerToolchainSources(evidence.toolchains);
|
|
2585
|
+
const declaredAssets = toolchains.flatMap((source) => source.descriptor.assets);
|
|
2586
|
+
const evidencePaths = Object.keys(evidence.toolchainAssets).sort();
|
|
2587
|
+
const declaredPaths = declaredAssets.map((asset) => asset.path).sort();
|
|
2588
|
+
if (evidencePaths.length !== declaredPaths.length || evidencePaths.some((assetPath, index) => assetPath !== declaredPaths[index])) throw new Error("Verified WASM-OJ distribution evidence does not exactly match its toolchain descriptors.");
|
|
2589
|
+
for (const asset of declaredAssets) if (evidence.toolchainAssets[asset.path] !== asset.sha256) throw new Error(`Verified WASM-OJ distribution evidence does not bind toolchain asset '${asset.path}'.`);
|
|
2590
|
+
const toolchainAssetFiles = serverToolchainAssetFiles(toolchains, declaredPaths);
|
|
2591
|
+
const token = Object.freeze({
|
|
2592
|
+
compilerExecutable: path.resolve(evidence.compilerExecutable),
|
|
2593
|
+
runtimeExecutable: path.resolve(evidence.runtimeExecutable),
|
|
2594
|
+
toolchainAssetFiles,
|
|
2595
|
+
toolchainRootSha256: evidence.toolchainRootSha256
|
|
2596
|
+
});
|
|
2597
|
+
verifiedDistributions.add(token);
|
|
2598
|
+
return token;
|
|
2599
|
+
}
|
|
2600
|
+
function assertVerifiedServerDistribution(token, paths, toolchains) {
|
|
2601
|
+
if (!verifiedDistributions.has(token) || token.compilerExecutable !== path.resolve(paths.compilerExecutable) || token.runtimeExecutable !== path.resolve(paths.runtimeExecutable)) throw new Error("WASM-OJ verified-distribution token does not authorize the resolved server runtime.");
|
|
2602
|
+
assertVerifiedToolchainDistribution(token, toolchains);
|
|
2603
|
+
}
|
|
2604
|
+
function assertVerifiedToolchainDistribution(token, toolchains) {
|
|
2605
|
+
if (!verifiedDistributions.has(token)) throw new Error("WASM-OJ verified-distribution token is not process-authentic.");
|
|
2606
|
+
const actual = serverToolchainAssetFiles(toolchains, toolchains.flatMap((source) => source.descriptor.assets.map((asset) => asset.path)));
|
|
2607
|
+
if (!sameRecord(token.toolchainAssetFiles, actual)) throw new Error("WASM-OJ verified-distribution token does not authorize these toolchain sources.");
|
|
2608
|
+
}
|
|
2609
|
+
function sameRecord(expected, actual) {
|
|
2610
|
+
const expectedEntries = Object.entries(expected).sort(([left], [right]) => left.localeCompare(right));
|
|
2611
|
+
const actualEntries = Object.entries(actual).sort(([left], [right]) => left.localeCompare(right));
|
|
2612
|
+
return expectedEntries.length === actualEntries.length && expectedEntries.every(([key, value], index) => {
|
|
2613
|
+
const candidate = actualEntries[index];
|
|
2614
|
+
return candidate?.[0] === key && candidate[1] === value;
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
//#endregion
|
|
2618
|
+
//#region src/server/stage-scripts.ts
|
|
2619
|
+
var SERVER_STAGE_SCRIPTS = Object.freeze([
|
|
2620
|
+
"server-build-stage.mjs",
|
|
2621
|
+
"server-runner-stage.mjs",
|
|
2622
|
+
"python-stage.mjs",
|
|
2623
|
+
"rustc-stage.mjs",
|
|
2624
|
+
"go-stage.mjs",
|
|
2625
|
+
"java-stage.mjs"
|
|
2626
|
+
]);
|
|
2627
|
+
var SERVER_STAGE_SCRIPT_SET = new Set(SERVER_STAGE_SCRIPTS);
|
|
2628
|
+
/** Resolve the one package-owned directory that contains every isolated server stage. */
|
|
2629
|
+
function resolveServerStageDirectory(moduleUrl = import.meta.url) {
|
|
2630
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
2631
|
+
const moduleDirectory = path.dirname(modulePath);
|
|
2632
|
+
const moduleFilename = path.basename(modulePath);
|
|
2633
|
+
if (moduleFilename === "stage-scripts.ts" && path.basename(moduleDirectory) === "server" && path.basename(path.dirname(moduleDirectory)) === "src") return moduleDirectory;
|
|
2634
|
+
if ((moduleFilename === "index.js" || moduleFilename === "server-build-stage.mjs") && path.basename(moduleDirectory) === "dist") return moduleDirectory;
|
|
2635
|
+
throw new Error(`Unsupported @wasm-oj/server module layout '${modulePath}'.`);
|
|
2636
|
+
}
|
|
2637
|
+
/** Resolve only a declared stage below an already-established package stage root. */
|
|
2638
|
+
function serverStageScript(stageDirectory, scriptName) {
|
|
2639
|
+
if (!path.isAbsolute(stageDirectory)) throw new Error("The @wasm-oj/server stage directory must be absolute.");
|
|
2640
|
+
if (!SERVER_STAGE_SCRIPT_SET.has(scriptName)) throw new Error(`Unknown isolated server stage '${scriptName}'.`);
|
|
2641
|
+
return path.join(stageDirectory, scriptName);
|
|
2642
|
+
}
|
|
2643
|
+
//#endregion
|
|
2644
|
+
//#region src/server/server-compiler.ts
|
|
2645
|
+
var IN_PROCESS_STAGE = Symbol("wasm-oj-in-process-server-compiler");
|
|
2646
|
+
var SERVER_STAGE_LOG_LIMIT_BYTES = 1048576;
|
|
2647
|
+
var SERVER_STAGE_PROGRESS_LINE_LIMIT_BYTES = 1048576;
|
|
2648
|
+
var SERVER_BUILD_RESPONSE_LIMIT_BYTES = 268435456;
|
|
2649
|
+
var SERVER_BUILD_REQUEST_LIMIT_BYTES = 805306368;
|
|
2650
|
+
var SERVER_COMPILER_STAGE_RESPONSE_LIMIT_BYTES = 268435456;
|
|
2651
|
+
/**
|
|
2652
|
+
* Node/server compiler host using the exact language drivers and Wasmer
|
|
2653
|
+
* packages used by the browser Worker.
|
|
2654
|
+
*/
|
|
2655
|
+
var ServerCompiler = class {
|
|
2656
|
+
progressListeners = /* @__PURE__ */ new Set();
|
|
2657
|
+
compilerExecutable;
|
|
2658
|
+
toolchains;
|
|
2659
|
+
initialization;
|
|
2660
|
+
generation = 0;
|
|
2661
|
+
disposed = false;
|
|
2662
|
+
inProcess;
|
|
2663
|
+
verifiedToolchain;
|
|
2664
|
+
stageDirectory;
|
|
2665
|
+
activeChildren = /* @__PURE__ */ new Set();
|
|
2666
|
+
activeOperation;
|
|
2667
|
+
constructor(options, stage) {
|
|
2668
|
+
this.compilerExecutable = path.resolve(options.compilerExecutable);
|
|
2669
|
+
this.toolchains = snapshotServerToolchainSources(options.toolchains);
|
|
2670
|
+
if (options.verifiedDistribution) assertVerifiedToolchainDistribution(options.verifiedDistribution, this.toolchains);
|
|
2671
|
+
if (options.verifiedToolchain === true && stage !== IN_PROCESS_STAGE) throw new Error("Verified toolchain inheritance is reserved for the isolated compiler stage.");
|
|
2672
|
+
this.verifiedToolchain = options.verifiedDistribution !== void 0 || options.verifiedToolchain === true;
|
|
2673
|
+
this.inProcess = stage === IN_PROCESS_STAGE;
|
|
2674
|
+
if (stage === IN_PROCESS_STAGE) {
|
|
2675
|
+
const inheritedStageDirectory = options.stageDirectory;
|
|
2676
|
+
if (typeof inheritedStageDirectory !== "string" || !path.isAbsolute(inheritedStageDirectory)) throw new Error("The inherited @wasm-oj/server stage directory must be absolute.");
|
|
2677
|
+
this.stageDirectory = inheritedStageDirectory;
|
|
2678
|
+
} else this.stageDirectory = resolveServerStageDirectory();
|
|
2679
|
+
}
|
|
2680
|
+
cacheIdentity(project) {
|
|
2681
|
+
this.assertActive();
|
|
2682
|
+
assertServerToolchainProfile(this.toolchains, project.config.language, project.config.target, project.config.optimization);
|
|
2683
|
+
return JSON.stringify(toolchainCacheIdentity(project.config.language));
|
|
2684
|
+
}
|
|
2685
|
+
async ready() {
|
|
2686
|
+
this.assertActive();
|
|
2687
|
+
const generation = this.generation;
|
|
2688
|
+
let initialization = this.initialization;
|
|
2689
|
+
if (!initialization) {
|
|
2690
|
+
initialization = this.initialize();
|
|
2691
|
+
this.initialization = initialization;
|
|
2692
|
+
initialization.catch(() => {
|
|
2693
|
+
if (this.initialization === initialization) this.initialization = void 0;
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
await initialization;
|
|
2697
|
+
this.assertActive();
|
|
2698
|
+
if (generation !== this.generation) throw new Error("Server compiler initialization was superseded.");
|
|
2699
|
+
}
|
|
2700
|
+
async build(project, cacheKey) {
|
|
2701
|
+
assertValidProject(project);
|
|
2702
|
+
assertCompilerCacheKey(cacheKey);
|
|
2703
|
+
const operation = this.beginOperation("build");
|
|
2704
|
+
try {
|
|
2705
|
+
await this.ready();
|
|
2706
|
+
this.assertCurrent(operation, "Server compilation was cancelled before initialization completed.");
|
|
2707
|
+
if (!this.inProcess) return await this.buildIsolated(project, cacheKey, operation);
|
|
2708
|
+
const runtime = new Runtime({ registry: null });
|
|
2709
|
+
configureWasmerCompilerHost({
|
|
2710
|
+
getRuntime: () => runtime,
|
|
2711
|
+
loadToolchainAsset: (assetPath) => this.loadToolchainAsset(assetPath),
|
|
2712
|
+
loadToolchainFile: (assetPath) => this.loadToolchainFile(assetPath),
|
|
2713
|
+
compileRust: (request) => this.compileRust(request),
|
|
2714
|
+
compilePython: (request) => this.compilePython(request),
|
|
2715
|
+
compileGo: (request) => this.compileGo(request),
|
|
2716
|
+
compileJava: (request) => this.compileJava(request),
|
|
2717
|
+
progress: (_requestId, phase, label, value) => {
|
|
2718
|
+
if (!this.isCurrent(operation)) return;
|
|
2719
|
+
const progress = {
|
|
2720
|
+
phase,
|
|
2721
|
+
label,
|
|
2722
|
+
progress: value
|
|
2723
|
+
};
|
|
2724
|
+
for (const listener of this.progressListeners) listener(progress);
|
|
2725
|
+
},
|
|
2726
|
+
trace: () => void 0
|
|
2727
|
+
});
|
|
2728
|
+
try {
|
|
2729
|
+
const result = await buildProject(project, cacheKey, crypto.randomUUID());
|
|
2730
|
+
this.assertCurrent(operation, "Server compilation was cancelled.");
|
|
2731
|
+
return result;
|
|
2732
|
+
} finally {
|
|
2733
|
+
await clearSdkDirectClangCaches();
|
|
2734
|
+
runtime.free();
|
|
2735
|
+
}
|
|
2736
|
+
} finally {
|
|
2737
|
+
this.endOperation(operation);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
onProgress(listener) {
|
|
2741
|
+
this.assertActive();
|
|
2742
|
+
this.progressListeners.add(listener);
|
|
2743
|
+
return () => this.progressListeners.delete(listener);
|
|
2744
|
+
}
|
|
2745
|
+
async clearToolchainCache() {
|
|
2746
|
+
const operation = this.beginOperation("cache-clear");
|
|
2747
|
+
try {
|
|
2748
|
+
await this.ready();
|
|
2749
|
+
this.assertCurrent(operation, "Server compiler cache clearing was superseded.");
|
|
2750
|
+
if (this.inProcess) {
|
|
2751
|
+
clearCompilerHostCaches();
|
|
2752
|
+
await clearSdkDirectClangCaches();
|
|
2753
|
+
this.assertCurrent(operation, "Server compiler cache clearing was superseded.");
|
|
2754
|
+
}
|
|
2755
|
+
} finally {
|
|
2756
|
+
this.endOperation(operation);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
cancel() {
|
|
2760
|
+
if (this.disposed) return;
|
|
2761
|
+
if (this.activeOperation?.kind === "cache-clear") return;
|
|
2762
|
+
this.generation += 1;
|
|
2763
|
+
if (this.activeOperation) {
|
|
2764
|
+
this.activeOperation.superseded = true;
|
|
2765
|
+
this.activeOperation = void 0;
|
|
2766
|
+
}
|
|
2767
|
+
this.terminateChildren();
|
|
2768
|
+
}
|
|
2769
|
+
restart() {
|
|
2770
|
+
this.assertActive();
|
|
2771
|
+
if (this.activeOperation?.kind === "cache-clear") throw new Error("Cannot restart ServerCompiler while clearing its cache.");
|
|
2772
|
+
this.cancel();
|
|
2773
|
+
if (this.inProcess) clearCompilerHostCaches();
|
|
2774
|
+
}
|
|
2775
|
+
dispose() {
|
|
2776
|
+
if (this.disposed) return;
|
|
2777
|
+
this.disposed = true;
|
|
2778
|
+
this.generation += 1;
|
|
2779
|
+
if (this.activeOperation) {
|
|
2780
|
+
this.activeOperation.superseded = true;
|
|
2781
|
+
this.activeOperation = void 0;
|
|
2782
|
+
}
|
|
2783
|
+
this.terminateChildren();
|
|
2784
|
+
this.progressListeners.clear();
|
|
2785
|
+
}
|
|
2786
|
+
async initialize() {
|
|
2787
|
+
await Promise.all([
|
|
2788
|
+
access(this.compilerExecutable, constants.X_OK),
|
|
2789
|
+
...serverToolchainDirectories(this.toolchains).map((directory) => access(directory, constants.R_OK)),
|
|
2790
|
+
this.inProcess ? initializeServerWasmerSdk() : Promise.resolve()
|
|
2791
|
+
]);
|
|
2792
|
+
}
|
|
2793
|
+
async buildIsolated(project, cacheKey, operation) {
|
|
2794
|
+
const transportDirectory = await mkdtemp(path.join(os.tmpdir(), "wasm-oj-build-response-"));
|
|
2795
|
+
const responsePath = path.join(transportDirectory, "response.v8");
|
|
2796
|
+
const requestPath = path.join(transportDirectory, "request.v8");
|
|
2797
|
+
const timeoutMs = buildControlTimeoutMs(project.config.language);
|
|
2798
|
+
try {
|
|
2799
|
+
this.assertCurrent(operation, "Server compilation was cancelled before its isolated stage started.");
|
|
2800
|
+
const encodedRequest = serialize({
|
|
2801
|
+
compilerExecutable: this.compilerExecutable,
|
|
2802
|
+
stageDirectory: this.stageDirectory,
|
|
2803
|
+
toolchains: serializeServerToolchainSources(this.toolchains),
|
|
2804
|
+
verifiedToolchain: this.verifiedToolchain,
|
|
2805
|
+
project,
|
|
2806
|
+
cacheKey
|
|
2807
|
+
});
|
|
2808
|
+
if (encodedRequest.byteLength > SERVER_BUILD_REQUEST_LIMIT_BYTES) throw new Error(`Server compiler request exceeds ${SERVER_BUILD_REQUEST_LIMIT_BYTES} bytes.`);
|
|
2809
|
+
await writeFile(requestPath, encodedRequest, {
|
|
2810
|
+
flag: "wx",
|
|
2811
|
+
mode: 384
|
|
2812
|
+
});
|
|
2813
|
+
return await new Promise((resolve, reject) => {
|
|
2814
|
+
const script = serverStageScript(this.stageDirectory, "server-build-stage.mjs");
|
|
2815
|
+
const child = spawn(process.execPath, [
|
|
2816
|
+
"--experimental-strip-types",
|
|
2817
|
+
"--disable-warning=ExperimentalWarning",
|
|
2818
|
+
script
|
|
2819
|
+
], {
|
|
2820
|
+
stdio: [
|
|
2821
|
+
"pipe",
|
|
2822
|
+
"pipe",
|
|
2823
|
+
"pipe",
|
|
2824
|
+
"pipe"
|
|
2825
|
+
],
|
|
2826
|
+
env: {
|
|
2827
|
+
...process.env,
|
|
2828
|
+
WASM_OJ_BUILD_REQUEST: requestPath,
|
|
2829
|
+
WASM_OJ_BUILD_RESPONSE: responsePath
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
this.activeChildren.add(child);
|
|
2833
|
+
let progressBuffer = "";
|
|
2834
|
+
let timedOut = false;
|
|
2835
|
+
let transportError;
|
|
2836
|
+
const failTransport = (error) => {
|
|
2837
|
+
transportError ??= error;
|
|
2838
|
+
child.kill("SIGKILL");
|
|
2839
|
+
};
|
|
2840
|
+
const stdout = new BoundedByteCollector("Isolated server compiler stdout", SERVER_STAGE_LOG_LIMIT_BYTES, failTransport);
|
|
2841
|
+
const stderr = new BoundedByteCollector("Isolated server compiler stderr", SERVER_STAGE_LOG_LIMIT_BYTES, failTransport);
|
|
2842
|
+
child.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
2843
|
+
child.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
2844
|
+
child.on("error", (error) => {
|
|
2845
|
+
transportError = error;
|
|
2846
|
+
});
|
|
2847
|
+
child.stdin.on("error", (error) => {
|
|
2848
|
+
transportError ??= error;
|
|
2849
|
+
});
|
|
2850
|
+
const progressStream = child.stdio[3];
|
|
2851
|
+
if (!progressStream || typeof progressStream === "number") {
|
|
2852
|
+
child.kill("SIGKILL");
|
|
2853
|
+
this.activeChildren.delete(child);
|
|
2854
|
+
reject(/* @__PURE__ */ new Error("The isolated server compiler did not expose its progress channel."));
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
progressStream.on("data", (chunk) => {
|
|
2858
|
+
if (Buffer.byteLength(progressBuffer, "utf8") + chunk.byteLength > SERVER_STAGE_PROGRESS_LINE_LIMIT_BYTES) {
|
|
2859
|
+
failTransport(/* @__PURE__ */ new Error(`Isolated server compiler progress exceeded the ${SERVER_STAGE_PROGRESS_LINE_LIMIT_BYTES} byte line boundary.`));
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
progressBuffer += chunk.toString();
|
|
2863
|
+
const lines = progressBuffer.split("\n");
|
|
2864
|
+
progressBuffer = lines.pop() ?? "";
|
|
2865
|
+
for (const line of lines) {
|
|
2866
|
+
if (!line) continue;
|
|
2867
|
+
try {
|
|
2868
|
+
const progress = JSON.parse(line);
|
|
2869
|
+
if (this.isCurrent(operation)) for (const listener of this.progressListeners) listener(progress);
|
|
2870
|
+
} catch {}
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
const timer = setTimeout(() => {
|
|
2874
|
+
timedOut = true;
|
|
2875
|
+
child.kill("SIGKILL");
|
|
2876
|
+
}, timeoutMs);
|
|
2877
|
+
child.on("close", async () => {
|
|
2878
|
+
clearTimeout(timer);
|
|
2879
|
+
this.activeChildren.delete(child);
|
|
2880
|
+
try {
|
|
2881
|
+
this.assertCurrent(operation, "Server compilation was cancelled.");
|
|
2882
|
+
if (timedOut) throw new Error(`Server compilation exceeded ${timeoutMs} ms.`);
|
|
2883
|
+
if (transportError) throw transportError;
|
|
2884
|
+
let encodedResponse;
|
|
2885
|
+
try {
|
|
2886
|
+
encodedResponse = await readBoundedRegularFile(responsePath, SERVER_BUILD_RESPONSE_LIMIT_BYTES);
|
|
2887
|
+
} catch (error) {
|
|
2888
|
+
const stageError = stderr.text().trim() || stdout.text().trim();
|
|
2889
|
+
if (stageError) throw new Error(stageError, { cause: error });
|
|
2890
|
+
throw error;
|
|
2891
|
+
}
|
|
2892
|
+
const response = deserialize(encodedResponse);
|
|
2893
|
+
if (!response.ok || !response.result) throw new Error(response.error || stderr.text() || stdout.text() || "The isolated server compiler failed.");
|
|
2894
|
+
resolve(response.result);
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
reject(error);
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
child.stdin.end();
|
|
2900
|
+
});
|
|
2901
|
+
} finally {
|
|
2902
|
+
await rm(transportDirectory, {
|
|
2903
|
+
recursive: true,
|
|
2904
|
+
force: true
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
async compileRust(request) {
|
|
2909
|
+
const result = await this.runCompilerStage("rustc-stage.mjs", { request }, RUST_COMPILE_TIMEOUT_MS, [RUST_TOOLCHAIN.packageAsset, RUST_TOOLCHAIN.manifestAsset]);
|
|
2910
|
+
return {
|
|
2911
|
+
...result,
|
|
2912
|
+
diagnostics: parseRustDiagnostics(result.stderr),
|
|
2913
|
+
wasm: result.wasmBase64 ? new Uint8Array(Buffer.from(result.wasmBase64, "base64")) : void 0
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
async compilePython(request) {
|
|
2917
|
+
const result = await this.runCompilerStage("python-stage.mjs", { request }, PYTHON_COMPILE_TIMEOUT_MS, [PYTHON_PACKAGE_ASSET_PATH]);
|
|
2918
|
+
return {
|
|
2919
|
+
...result,
|
|
2920
|
+
bytecode: Object.fromEntries(Object.entries(result.bytecodeBase64).map(([path, base64]) => [path, new Uint8Array(Buffer.from(base64, "base64"))])),
|
|
2921
|
+
diagnostics: parsePythonDiagnostics(`${result.stderr}\n${result.stdout}`)
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
async compileGo(request) {
|
|
2925
|
+
const result = await this.runCompilerStage("go-stage.mjs", {
|
|
2926
|
+
compilerExecutable: this.compilerExecutable,
|
|
2927
|
+
compileBatchSchema: WASM_OJ_SCHEMAS.compileBatch,
|
|
2928
|
+
request
|
|
2929
|
+
}, GO_COMPILE_TIMEOUT_MS, [
|
|
2930
|
+
GO_TOOLCHAIN.packageAsset,
|
|
2931
|
+
GO_TOOLCHAIN.manifestAsset,
|
|
2932
|
+
GO_TOOLCHAIN.standardLibraryAsset
|
|
2933
|
+
]);
|
|
2934
|
+
return {
|
|
2935
|
+
...result,
|
|
2936
|
+
diagnostics: parseGoDiagnostics(result.stderr),
|
|
2937
|
+
wasm: result.wasmBase64 ? new Uint8Array(Buffer.from(result.wasmBase64, "base64")) : void 0
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
async compileJava(request) {
|
|
2941
|
+
const result = await this.runCompilerStage("java-stage.mjs", { request }, JAVA_COMPILE_TIMEOUT_MS, [
|
|
2942
|
+
JAVA_COMPILER_ASSET_PATH,
|
|
2943
|
+
JAVA_COMPILE_CLASSLIB_ASSET_PATH,
|
|
2944
|
+
JAVA_RUNTIME_CLASSLIB_ASSET_PATH
|
|
2945
|
+
]);
|
|
2946
|
+
return {
|
|
2947
|
+
...result,
|
|
2948
|
+
wasm: result.wasmBase64 ? new Uint8Array(Buffer.from(result.wasmBase64, "base64")) : void 0
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
runCompilerStage(scriptName, input, timeoutMs, assetPaths) {
|
|
2952
|
+
const operation = this.activeOperation;
|
|
2953
|
+
if (!operation || operation.kind !== "build") return Promise.reject(/* @__PURE__ */ new Error("Server compilation was cancelled before its compiler stage started."));
|
|
2954
|
+
this.assertCurrent(operation, "Server compilation was cancelled before its compiler stage started.");
|
|
2955
|
+
return new Promise((resolve, reject) => {
|
|
2956
|
+
const script = serverStageScript(this.stageDirectory, scriptName);
|
|
2957
|
+
const child = spawn(process.execPath, [
|
|
2958
|
+
"--experimental-strip-types",
|
|
2959
|
+
"--disable-warning=ExperimentalWarning",
|
|
2960
|
+
script
|
|
2961
|
+
], { stdio: [
|
|
2962
|
+
"pipe",
|
|
2963
|
+
"pipe",
|
|
2964
|
+
"pipe",
|
|
2965
|
+
"pipe"
|
|
2966
|
+
] });
|
|
2967
|
+
this.activeChildren.add(child);
|
|
2968
|
+
let transportError;
|
|
2969
|
+
let timedOut = false;
|
|
2970
|
+
const failTransport = (error) => {
|
|
2971
|
+
transportError ??= error;
|
|
2972
|
+
child.kill("SIGKILL");
|
|
2973
|
+
};
|
|
2974
|
+
const stdout = new BoundedByteCollector(`Isolated compiler stage '${scriptName}' stdout`, SERVER_STAGE_LOG_LIMIT_BYTES, failTransport);
|
|
2975
|
+
const stderr = new BoundedByteCollector(`Isolated compiler stage '${scriptName}' stderr`, SERVER_STAGE_LOG_LIMIT_BYTES, failTransport);
|
|
2976
|
+
const responseBytes = new BoundedByteCollector(`Isolated compiler stage '${scriptName}' response`, SERVER_COMPILER_STAGE_RESPONSE_LIMIT_BYTES, failTransport);
|
|
2977
|
+
child.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
2978
|
+
child.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
2979
|
+
child.on("error", (error) => {
|
|
2980
|
+
transportError = error;
|
|
2981
|
+
});
|
|
2982
|
+
child.stdin.on("error", (error) => {
|
|
2983
|
+
transportError ??= error;
|
|
2984
|
+
});
|
|
2985
|
+
const responseStream = child.stdio[3];
|
|
2986
|
+
if (!responseStream || typeof responseStream === "number") {
|
|
2987
|
+
child.kill("SIGKILL");
|
|
2988
|
+
this.activeChildren.delete(child);
|
|
2989
|
+
reject(/* @__PURE__ */ new Error(`The isolated compiler stage '${scriptName}' did not expose its response channel.`));
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
responseStream.on("data", (chunk) => responseBytes.append(chunk));
|
|
2993
|
+
const timer = setTimeout(() => {
|
|
2994
|
+
timedOut = true;
|
|
2995
|
+
child.kill("SIGKILL");
|
|
2996
|
+
}, timeoutMs + 5e3);
|
|
2997
|
+
child.on("close", () => {
|
|
2998
|
+
clearTimeout(timer);
|
|
2999
|
+
this.activeChildren.delete(child);
|
|
3000
|
+
try {
|
|
3001
|
+
if (transportError) throw transportError;
|
|
3002
|
+
if (timedOut) throw new Error(`The isolated compiler stage '${scriptName}' exceeded ${timeoutMs + 5e3} ms.`);
|
|
3003
|
+
const response = JSON.parse(responseBytes.text());
|
|
3004
|
+
if (!response.ok || !response.result) throw new Error(response.error || stderr.text() || stdout.text() || `The isolated compiler stage '${scriptName}' failed.`);
|
|
3005
|
+
resolve(response.result);
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
reject(error);
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
child.stdin.end(JSON.stringify({
|
|
3011
|
+
toolchainAssets: serverToolchainAssetFiles(this.toolchains, assetPaths),
|
|
3012
|
+
verifiedToolchain: this.verifiedToolchain,
|
|
3013
|
+
...input
|
|
3014
|
+
}));
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
async loadToolchainAsset(assetPath) {
|
|
3018
|
+
const resolved = serverToolchainAssetFile(this.toolchains, assetPath);
|
|
3019
|
+
const compressed = await readFile(resolved);
|
|
3020
|
+
if (!this.verifiedToolchain) this.verifyToolchainAsset(assetPath, compressed);
|
|
3021
|
+
return uint8View(gunzipSync(compressed));
|
|
3022
|
+
}
|
|
3023
|
+
async loadToolchainFile(assetPath) {
|
|
3024
|
+
const resolved = serverToolchainAssetFile(this.toolchains, assetPath);
|
|
3025
|
+
const bytes = await readFile(resolved);
|
|
3026
|
+
if (!this.verifiedToolchain) this.verifyToolchainAsset(assetPath, bytes);
|
|
3027
|
+
return new Uint8Array(bytes);
|
|
3028
|
+
}
|
|
3029
|
+
verifyToolchainAsset(assetPath, bytes) {
|
|
3030
|
+
const expected = toolchainAssetSource(this.toolchains, assetPath).asset.sha256;
|
|
3031
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
3032
|
+
if (actual !== expected) throw new Error(`Pinned toolchain asset '${assetPath}' has digest ${actual}; expected ${expected}.`);
|
|
3033
|
+
}
|
|
3034
|
+
beginOperation(kind) {
|
|
3035
|
+
this.assertActive();
|
|
3036
|
+
if (this.activeOperation) throw new Error("ServerCompiler accepts one active operation at a time.");
|
|
3037
|
+
const operation = {
|
|
3038
|
+
kind,
|
|
3039
|
+
generation: this.generation,
|
|
3040
|
+
superseded: false
|
|
3041
|
+
};
|
|
3042
|
+
this.activeOperation = operation;
|
|
3043
|
+
return operation;
|
|
3044
|
+
}
|
|
3045
|
+
endOperation(operation) {
|
|
3046
|
+
if (this.activeOperation === operation) this.activeOperation = void 0;
|
|
3047
|
+
}
|
|
3048
|
+
assertCurrent(operation, message) {
|
|
3049
|
+
this.assertActive();
|
|
3050
|
+
if (!this.isCurrent(operation)) throw new Error(message);
|
|
3051
|
+
}
|
|
3052
|
+
isCurrent(operation) {
|
|
3053
|
+
return !this.disposed && !operation.superseded && operation.generation === this.generation;
|
|
3054
|
+
}
|
|
3055
|
+
terminateChildren() {
|
|
3056
|
+
for (const child of this.activeChildren) {
|
|
3057
|
+
if (this.inProcess) {
|
|
3058
|
+
child.kill("SIGKILL");
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
child.kill("SIGTERM");
|
|
3062
|
+
setTimeout(() => {
|
|
3063
|
+
if (this.activeChildren.has(child)) child.kill("SIGKILL");
|
|
3064
|
+
}, 1e3).unref();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
assertActive() {
|
|
3068
|
+
if (this.disposed) throw new Error("ServerCompiler is disposed.");
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
function uint8View(bytes) {
|
|
3072
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3073
|
+
}
|
|
3074
|
+
//#endregion
|
|
3075
|
+
//#region src/runner/runtime-files.ts
|
|
3076
|
+
var MAGIC = new TextEncoder().encode("WOJFS002");
|
|
3077
|
+
var HEADER_BYTES = 12;
|
|
3078
|
+
`${WASM_OJ_STORAGE.runtimeFilesCache}`;
|
|
3079
|
+
String.raw`
|
|
3080
|
+
import io
|
|
3081
|
+
import os
|
|
3082
|
+
import sys
|
|
3083
|
+
import zipfile
|
|
3084
|
+
|
|
3085
|
+
source_root = "/usr/local/lib/python3.14"
|
|
3086
|
+
guest_path = "/cpython/lib/python314.zip"
|
|
3087
|
+
output = sys.stdout.buffer
|
|
3088
|
+
output.write(b"WOJFS002")
|
|
3089
|
+
|
|
3090
|
+
archive_buffer = io.BytesIO()
|
|
3091
|
+
with zipfile.ZipFile(archive_buffer, "w", compression=zipfile.ZIP_STORED) as archive:
|
|
3092
|
+
for root, directories, files in os.walk(source_root):
|
|
3093
|
+
directories[:] = sorted(
|
|
3094
|
+
name for name in directories
|
|
3095
|
+
if name != "__pycache__" and not os.path.islink(os.path.join(root, name))
|
|
3096
|
+
)
|
|
3097
|
+
for name in sorted(files):
|
|
3098
|
+
source_path = os.path.join(root, name)
|
|
3099
|
+
if os.path.islink(source_path) or name.endswith((".pyc", ".pyo")):
|
|
3100
|
+
continue
|
|
3101
|
+
archive_path = os.path.relpath(source_path, source_root).replace(os.sep, "/")
|
|
3102
|
+
info = zipfile.ZipInfo(archive_path, date_time=(1980, 1, 1, 0, 0, 0))
|
|
3103
|
+
info.compress_type = zipfile.ZIP_STORED
|
|
3104
|
+
info.external_attr = 0o100644 << 16
|
|
3105
|
+
with open(source_path, "rb") as source:
|
|
3106
|
+
archive.writestr(info, source.read())
|
|
3107
|
+
|
|
3108
|
+
encoded_path = guest_path.encode("utf-8")
|
|
3109
|
+
archive_data = archive_buffer.getvalue()
|
|
3110
|
+
output.write(len(encoded_path).to_bytes(4, "little"))
|
|
3111
|
+
output.write(len(archive_data).to_bytes(8, "little"))
|
|
3112
|
+
output.write(encoded_path)
|
|
3113
|
+
output.write(archive_data)
|
|
3114
|
+
|
|
3115
|
+
output.write((0).to_bytes(4, "little"))
|
|
3116
|
+
output.write((0).to_bytes(8, "little"))
|
|
3117
|
+
`;
|
|
3118
|
+
function safeRuntimePath(path) {
|
|
3119
|
+
return path.startsWith("/") && !path.includes("\\") && !path.includes("//") && !path.endsWith("/") && !path.split("/").some((component) => component === "." || component === "..");
|
|
3120
|
+
}
|
|
3121
|
+
function decodeRuntimeFiles(archive) {
|
|
3122
|
+
if (archive.byteLength < MAGIC.byteLength + HEADER_BYTES) throw new Error("Runtime file archive is truncated.");
|
|
3123
|
+
for (let index = 0; index < MAGIC.byteLength; index += 1) if (archive[index] !== MAGIC[index]) throw new Error("Runtime file archive has an invalid signature.");
|
|
3124
|
+
const view = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
3125
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3126
|
+
const files = {};
|
|
3127
|
+
let offset = MAGIC.byteLength;
|
|
3128
|
+
while (true) {
|
|
3129
|
+
if (offset + HEADER_BYTES > archive.byteLength) throw new Error("Runtime file archive ended inside an entry header.");
|
|
3130
|
+
const pathLength = view.getUint32(offset, true);
|
|
3131
|
+
const dataLength = Number(view.getBigUint64(offset + 4, true));
|
|
3132
|
+
offset += HEADER_BYTES;
|
|
3133
|
+
if (pathLength === 0 && dataLength === 0) break;
|
|
3134
|
+
if (pathLength === 0 || !Number.isSafeInteger(dataLength)) throw new Error("Runtime file archive contains an invalid entry size.");
|
|
3135
|
+
const end = offset + pathLength + dataLength;
|
|
3136
|
+
if (!Number.isSafeInteger(end) || end > archive.byteLength) throw new Error("Runtime file archive entry exceeds the archive boundary.");
|
|
3137
|
+
const path = decoder.decode(archive.subarray(offset, offset + pathLength));
|
|
3138
|
+
offset += pathLength;
|
|
3139
|
+
if (!safeRuntimePath(path)) throw new Error(`Runtime file archive contains an unsafe path: '${path}'.`);
|
|
3140
|
+
if (Object.hasOwn(files, path)) throw new Error(`Runtime file archive contains duplicate path '${path}'.`);
|
|
3141
|
+
files[path] = archive.slice(offset, offset + dataLength);
|
|
3142
|
+
offset += dataLength;
|
|
3143
|
+
}
|
|
3144
|
+
if (offset !== archive.byteLength) throw new Error("Runtime file archive contains trailing bytes.");
|
|
3145
|
+
return files;
|
|
3146
|
+
}
|
|
3147
|
+
async function verifyAndDecodeRuntimeFiles(archive, expectedSha256) {
|
|
3148
|
+
if (!/^[a-f0-9]{64}$/.test(expectedSha256)) throw new Error("Runtime file archive expected SHA-256 must be 64 lowercase hexadecimal characters.");
|
|
3149
|
+
const actual = await sha256Hex(archive);
|
|
3150
|
+
if (actual !== expectedSha256) throw new Error(`Runtime file archive has digest ${actual}; expected ${expectedSha256}.`);
|
|
3151
|
+
return decodeRuntimeFiles(archive);
|
|
3152
|
+
}
|
|
3153
|
+
//#endregion
|
|
3154
|
+
//#region src/runner/preparation-timeout-policy.ts
|
|
3155
|
+
var DEFAULT_RUNTIME_PREPARATION_TIMEOUT_MS = 12e4;
|
|
3156
|
+
var PYTHON_RUNTIME_PREPARATION_TIMEOUT_MS = 3e5;
|
|
3157
|
+
/** Shared hard boundary for browser Worker and server-child runtime preparation. */
|
|
3158
|
+
function runtimePreparationTimeoutMs(artifact) {
|
|
3159
|
+
return artifact.language === "python" ? PYTHON_RUNTIME_PREPARATION_TIMEOUT_MS : DEFAULT_RUNTIME_PREPARATION_TIMEOUT_MS;
|
|
3160
|
+
}
|
|
3161
|
+
//#endregion
|
|
3162
|
+
//#region src/server/server-runner.ts
|
|
3163
|
+
var WASM_OJ_RUNTIME_CACHE_FILE = /^[0-9a-f]{64}\.wasmojfs$/;
|
|
3164
|
+
var WASM_OJ_RUNTIME_CACHE_TEMPORARY_FILE = /^[0-9a-f]{64}\.wasmojfs\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/;
|
|
3165
|
+
var SERVER_RUNNER_STAGE_SCRIPT = "server-runner-stage.mjs";
|
|
3166
|
+
var MAX_RUNNER_STAGE_RESPONSE_BYTES = 268435456;
|
|
3167
|
+
var MAX_RUNNER_STAGE_DIAGNOSTIC_BYTES = 1048576;
|
|
3168
|
+
var MAX_RUNTIME_CACHE_ARCHIVE_BYTES = 67108864;
|
|
3169
|
+
var MAX_NATIVE_CORE_DIAGNOSTIC_BYTES = 1048576;
|
|
3170
|
+
var NATIVE_CORE_RESPONSE_OVERHEAD_BYTES = 2097152;
|
|
3171
|
+
var ServerRunner = class {
|
|
3172
|
+
runtimeExecutable;
|
|
3173
|
+
toolchains;
|
|
3174
|
+
cacheDirectory;
|
|
3175
|
+
verifiedToolchain;
|
|
3176
|
+
stageDirectory;
|
|
3177
|
+
resolvedCacheDirectory;
|
|
3178
|
+
runtimeDrivers;
|
|
3179
|
+
packageCommands = /* @__PURE__ */ new Map();
|
|
3180
|
+
packageFileSystems = /* @__PURE__ */ new Map();
|
|
3181
|
+
progressListeners = /* @__PURE__ */ new Set();
|
|
3182
|
+
streamListeners = /* @__PURE__ */ new Set();
|
|
3183
|
+
initialization;
|
|
3184
|
+
activeNativeRuns = /* @__PURE__ */ new Set();
|
|
3185
|
+
activePreparationStages = /* @__PURE__ */ new Set();
|
|
3186
|
+
inFlightRuns = /* @__PURE__ */ new Set();
|
|
3187
|
+
activeOperation;
|
|
3188
|
+
cacheClearActive = false;
|
|
3189
|
+
generation = 0;
|
|
3190
|
+
disposed = false;
|
|
3191
|
+
constructor(options) {
|
|
3192
|
+
this.runtimeExecutable = path.resolve(options.runtimeExecutable);
|
|
3193
|
+
this.toolchains = snapshotServerToolchainSources(options.toolchains);
|
|
3194
|
+
if (options.verifiedDistribution) assertVerifiedToolchainDistribution(options.verifiedDistribution, this.toolchains);
|
|
3195
|
+
this.verifiedToolchain = options.verifiedDistribution !== void 0;
|
|
3196
|
+
this.stageDirectory = resolveServerStageDirectory();
|
|
3197
|
+
this.cacheDirectory = path.resolve(options.cacheDirectory);
|
|
3198
|
+
assertCacheDirectoryIsNotFilesystemRoot(this.cacheDirectory);
|
|
3199
|
+
if (options.runtimeDrivers && options.additionalCostBaselines) throw new Error("Provide either runtimeDrivers or additionalCostBaselines, not both.");
|
|
3200
|
+
this.runtimeDrivers = options.runtimeDrivers ?? createDefaultRuntimeDrivers(createExtendedCostBaselineRegistry(options.additionalCostBaselines));
|
|
3201
|
+
}
|
|
3202
|
+
async ready() {
|
|
3203
|
+
this.assertActive();
|
|
3204
|
+
const generation = this.generation;
|
|
3205
|
+
let initialization = this.initialization;
|
|
3206
|
+
if (!initialization) {
|
|
3207
|
+
initialization = this.initialize();
|
|
3208
|
+
this.initialization = initialization;
|
|
3209
|
+
initialization.catch(() => {
|
|
3210
|
+
if (this.initialization === initialization) this.initialization = void 0;
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
await initialization;
|
|
3214
|
+
this.assertActive();
|
|
3215
|
+
if (generation !== this.generation) throw new Error("Server runner initialization was superseded.");
|
|
3216
|
+
}
|
|
3217
|
+
async run(artifact, config) {
|
|
3218
|
+
this.assertActive();
|
|
3219
|
+
this.assertArtifactToolchain(artifact);
|
|
3220
|
+
if (this.activeOperation || this.cacheClearActive) throw new Error("ServerRunner accepts one active operation at a time.");
|
|
3221
|
+
const operation = createServerRunOperation(this.generation);
|
|
3222
|
+
this.activeOperation = operation;
|
|
3223
|
+
this.inFlightRuns.add(operation);
|
|
3224
|
+
try {
|
|
3225
|
+
await this.ready();
|
|
3226
|
+
this.assertCurrent(operation, "Server execution was cancelled before runtime preparation completed.");
|
|
3227
|
+
const started = performance.now();
|
|
3228
|
+
this.progress({
|
|
3229
|
+
phase: "loading-toolchain",
|
|
3230
|
+
label: `Resolving runtime for ${artifact.name}`,
|
|
3231
|
+
progress: .1
|
|
3232
|
+
});
|
|
3233
|
+
this.assertCurrent(operation, "Server execution was cancelled before runtime preparation started.");
|
|
3234
|
+
const prepared = await this.prepareWithDeadline(operation, artifact, config);
|
|
3235
|
+
this.assertCurrent(operation, "Server execution was cancelled during runtime preparation.");
|
|
3236
|
+
this.progress({
|
|
3237
|
+
phase: "running",
|
|
3238
|
+
label: `Running ${artifact.name} with native deterministic Wasmer`,
|
|
3239
|
+
progress: .25
|
|
3240
|
+
});
|
|
3241
|
+
this.assertCurrent(operation, "Server execution was cancelled before the native runtime started.");
|
|
3242
|
+
const result = await this.runNativeCore(prepared, config, started);
|
|
3243
|
+
this.assertCurrent(operation, "Server execution was cancelled before its result was delivered.");
|
|
3244
|
+
if (result.stdout) {
|
|
3245
|
+
this.stream("stdout", result.stdout);
|
|
3246
|
+
this.assertCurrent(operation, "Server execution was cancelled while its output was delivered.");
|
|
3247
|
+
}
|
|
3248
|
+
if (result.stderr) {
|
|
3249
|
+
this.stream("stderr", result.stderr);
|
|
3250
|
+
this.assertCurrent(operation, "Server execution was cancelled while its output was delivered.");
|
|
3251
|
+
}
|
|
3252
|
+
return result;
|
|
3253
|
+
} finally {
|
|
3254
|
+
await operation.quiesce();
|
|
3255
|
+
if (this.activeOperation === operation) this.activeOperation = void 0;
|
|
3256
|
+
this.inFlightRuns.delete(operation);
|
|
3257
|
+
operation.complete();
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
async runTrusted(program, config) {
|
|
3261
|
+
this.assertActive();
|
|
3262
|
+
if (this.activeOperation || this.cacheClearActive) throw new Error("ServerRunner accepts one active operation at a time.");
|
|
3263
|
+
const operation = createServerRunOperation(this.generation);
|
|
3264
|
+
this.activeOperation = operation;
|
|
3265
|
+
this.inFlightRuns.add(operation);
|
|
3266
|
+
try {
|
|
3267
|
+
await this.ready();
|
|
3268
|
+
this.assertCurrent(operation, "Trusted judge execution was cancelled before preparation completed.");
|
|
3269
|
+
const started = performance.now();
|
|
3270
|
+
const prepared = await this.prepareTrustedWithDeadline(operation, program, config);
|
|
3271
|
+
this.assertCurrent(operation, "Trusted judge execution was cancelled during preparation.");
|
|
3272
|
+
const result = await this.runNativeCore(prepared, config, started);
|
|
3273
|
+
this.assertCurrent(operation, "Trusted judge execution was cancelled before its result was delivered.");
|
|
3274
|
+
return result;
|
|
3275
|
+
} finally {
|
|
3276
|
+
await operation.quiesce();
|
|
3277
|
+
if (this.activeOperation === operation) this.activeOperation = void 0;
|
|
3278
|
+
this.inFlightRuns.delete(operation);
|
|
3279
|
+
operation.complete();
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
async interact(contestantArtifact, interactorArtifact, config) {
|
|
3283
|
+
this.assertActive();
|
|
3284
|
+
this.assertArtifactToolchain(contestantArtifact);
|
|
3285
|
+
this.assertArtifactToolchain(interactorArtifact);
|
|
3286
|
+
if (this.activeOperation || this.cacheClearActive) throw new Error("ServerRunner accepts one active operation at a time.");
|
|
3287
|
+
if (interactorArtifact.kind !== "wasm") throw new Error("Interactive judge artifacts must be standalone Wasm modules.");
|
|
3288
|
+
const operation = createServerRunOperation(this.generation);
|
|
3289
|
+
this.activeOperation = operation;
|
|
3290
|
+
this.inFlightRuns.add(operation);
|
|
3291
|
+
try {
|
|
3292
|
+
await this.ready();
|
|
3293
|
+
this.assertCurrent(operation, "Server interaction was cancelled before runtime preparation completed.");
|
|
3294
|
+
const started = performance.now();
|
|
3295
|
+
this.progress({
|
|
3296
|
+
phase: "loading-toolchain",
|
|
3297
|
+
label: "Resolving contestant and interactor runtimes",
|
|
3298
|
+
progress: .1
|
|
3299
|
+
});
|
|
3300
|
+
const [contestant, interactor] = await Promise.all([this.prepareInteractiveWithDeadline(operation, contestantArtifact, interactiveRunConfig(config.contestant, config.determinism)), this.prepareInteractiveWithDeadline(operation, interactorArtifact, interactiveRunConfig(config.interactor, config.determinism))]);
|
|
3301
|
+
this.assertCurrent(operation, "Server interaction was cancelled during runtime preparation.");
|
|
3302
|
+
this.progress({
|
|
3303
|
+
phase: "running",
|
|
3304
|
+
label: "Running interactive session with native deterministic Wasmer",
|
|
3305
|
+
progress: .25
|
|
3306
|
+
});
|
|
3307
|
+
const result = await this.runNativeInteractive(contestant, interactor, config, started);
|
|
3308
|
+
this.assertCurrent(operation, "Server interaction was cancelled before its result was delivered.");
|
|
3309
|
+
return result;
|
|
3310
|
+
} finally {
|
|
3311
|
+
await operation.quiesce();
|
|
3312
|
+
if (this.activeOperation === operation) this.activeOperation = void 0;
|
|
3313
|
+
this.inFlightRuns.delete(operation);
|
|
3314
|
+
operation.complete();
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async interactTrusted(contestantArtifact, interactorProgram, config) {
|
|
3318
|
+
this.assertActive();
|
|
3319
|
+
this.assertArtifactToolchain(contestantArtifact);
|
|
3320
|
+
if (this.activeOperation || this.cacheClearActive) throw new Error("ServerRunner accepts one active operation at a time.");
|
|
3321
|
+
const operation = createServerRunOperation(this.generation);
|
|
3322
|
+
this.activeOperation = operation;
|
|
3323
|
+
this.inFlightRuns.add(operation);
|
|
3324
|
+
try {
|
|
3325
|
+
await this.ready();
|
|
3326
|
+
this.assertCurrent(operation, "Trusted interaction was cancelled before preparation completed.");
|
|
3327
|
+
const started = performance.now();
|
|
3328
|
+
const [contestant, interactor] = await Promise.all([this.prepareInteractiveWithDeadline(operation, contestantArtifact, interactiveRunConfig(config.contestant, config.determinism)), this.prepareTrustedWithDeadline(operation, interactorProgram, interactiveRunConfig(config.interactor, config.determinism))]);
|
|
3329
|
+
this.assertCurrent(operation, "Trusted interaction was cancelled during preparation.");
|
|
3330
|
+
return await this.runNativeInteractive(contestant, interactor, config, started);
|
|
3331
|
+
} finally {
|
|
3332
|
+
await operation.quiesce();
|
|
3333
|
+
if (this.activeOperation === operation) this.activeOperation = void 0;
|
|
3334
|
+
this.inFlightRuns.delete(operation);
|
|
3335
|
+
operation.complete();
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
onProgress(listener) {
|
|
3339
|
+
this.assertActive();
|
|
3340
|
+
this.progressListeners.add(listener);
|
|
3341
|
+
return () => this.progressListeners.delete(listener);
|
|
3342
|
+
}
|
|
3343
|
+
onStream(listener) {
|
|
3344
|
+
this.assertActive();
|
|
3345
|
+
this.streamListeners.add(listener);
|
|
3346
|
+
return () => this.streamListeners.delete(listener);
|
|
3347
|
+
}
|
|
3348
|
+
async clearRuntimeCache() {
|
|
3349
|
+
this.assertActive();
|
|
3350
|
+
if (this.cacheClearActive || this.activeOperation || this.inFlightRuns.size > 0) throw new Error("Cannot clear the runtime cache while execution is still in flight.");
|
|
3351
|
+
this.cacheClearActive = true;
|
|
3352
|
+
const generation = this.generation;
|
|
3353
|
+
try {
|
|
3354
|
+
await this.ready();
|
|
3355
|
+
this.assertActive();
|
|
3356
|
+
if (generation !== this.generation) throw new Error("Server runtime cache clearing was superseded.");
|
|
3357
|
+
this.packageCommands.clear();
|
|
3358
|
+
this.packageFileSystems.clear();
|
|
3359
|
+
await removeWasmOjRuntimeCacheFiles(this.runtimeCacheDirectory());
|
|
3360
|
+
this.assertActive();
|
|
3361
|
+
if (generation !== this.generation) throw new Error("Server runtime cache clearing was superseded.");
|
|
3362
|
+
} finally {
|
|
3363
|
+
this.cacheClearActive = false;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
cancel() {
|
|
3367
|
+
if (this.disposed) return;
|
|
3368
|
+
if (this.cacheClearActive) return;
|
|
3369
|
+
this.generation += 1;
|
|
3370
|
+
if (this.activeOperation) {
|
|
3371
|
+
this.activeOperation.superseded = true;
|
|
3372
|
+
this.activeOperation.cancel(/* @__PURE__ */ new Error("Server execution was superseded by cancellation."));
|
|
3373
|
+
this.activeOperation = void 0;
|
|
3374
|
+
}
|
|
3375
|
+
this.abortPreparationStages(/* @__PURE__ */ new Error("Server runtime preparation was cancelled."));
|
|
3376
|
+
this.terminateActiveNativeRuns(/* @__PURE__ */ new Error("Server execution was cancelled."));
|
|
3377
|
+
}
|
|
3378
|
+
async cancelAndWait() {
|
|
3379
|
+
const completions = [...this.inFlightRuns].map((operation) => operation.completion);
|
|
3380
|
+
this.cancel();
|
|
3381
|
+
await Promise.all(completions);
|
|
3382
|
+
}
|
|
3383
|
+
restart() {
|
|
3384
|
+
this.assertActive();
|
|
3385
|
+
if (this.cacheClearActive) throw new Error("Cannot restart ServerRunner while clearing its cache.");
|
|
3386
|
+
this.cancel();
|
|
3387
|
+
this.packageCommands.clear();
|
|
3388
|
+
this.packageFileSystems.clear();
|
|
3389
|
+
}
|
|
3390
|
+
dispose() {
|
|
3391
|
+
if (this.disposed) return;
|
|
3392
|
+
this.disposed = true;
|
|
3393
|
+
this.generation += 1;
|
|
3394
|
+
if (this.activeOperation) {
|
|
3395
|
+
this.activeOperation.superseded = true;
|
|
3396
|
+
this.activeOperation.cancel(/* @__PURE__ */ new Error("Server execution was cancelled because its runner was disposed."));
|
|
3397
|
+
this.activeOperation = void 0;
|
|
3398
|
+
}
|
|
3399
|
+
this.abortPreparationStages(/* @__PURE__ */ new Error("Server runtime preparation was cancelled because its runner was disposed."));
|
|
3400
|
+
this.terminateActiveNativeRuns(/* @__PURE__ */ new Error("Server execution was cancelled because its runner was disposed."));
|
|
3401
|
+
this.progressListeners.clear();
|
|
3402
|
+
this.streamListeners.clear();
|
|
3403
|
+
this.packageCommands.clear();
|
|
3404
|
+
this.packageFileSystems.clear();
|
|
3405
|
+
}
|
|
3406
|
+
async initialize() {
|
|
3407
|
+
await access(this.runtimeExecutable, constants.X_OK);
|
|
3408
|
+
await mkdir(this.cacheDirectory, { recursive: true });
|
|
3409
|
+
const resolvedCacheDirectory = await realpath(this.cacheDirectory);
|
|
3410
|
+
assertCacheDirectoryIsNotFilesystemRoot(resolvedCacheDirectory);
|
|
3411
|
+
this.resolvedCacheDirectory = resolvedCacheDirectory;
|
|
3412
|
+
await Promise.all(serverToolchainDirectories(this.toolchains).map((directory) => access(directory, constants.R_OK)));
|
|
3413
|
+
}
|
|
3414
|
+
runtimeCacheDirectory() {
|
|
3415
|
+
if (!this.resolvedCacheDirectory) throw new Error("ServerRunner runtime cache is not initialized.");
|
|
3416
|
+
return this.resolvedCacheDirectory;
|
|
3417
|
+
}
|
|
3418
|
+
resolver(operation) {
|
|
3419
|
+
return {
|
|
3420
|
+
quickJs: () => operation.track(this.loadQuickJsForOperation(operation)),
|
|
3421
|
+
packageCommand: (packageSpecifier, commandName) => this.packageCommand(operation, packageSpecifier, commandName),
|
|
3422
|
+
packageFileSystem: (request) => this.packageFileSystem(operation, request)
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
async prepareWithDeadline(operation, artifact, config) {
|
|
3426
|
+
const timeoutMs = runtimePreparationTimeoutMs(artifact);
|
|
3427
|
+
let timer;
|
|
3428
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
3429
|
+
timer = setTimeout(() => {
|
|
3430
|
+
const error = /* @__PURE__ */ new Error(`Server runtime preparation exceeded ${timeoutMs} ms.`);
|
|
3431
|
+
operation.superseded = true;
|
|
3432
|
+
this.abortPreparationStages(error, operation);
|
|
3433
|
+
reject(error);
|
|
3434
|
+
}, timeoutMs);
|
|
3435
|
+
});
|
|
3436
|
+
try {
|
|
3437
|
+
return await Promise.race([
|
|
3438
|
+
prepareArtifactRun(artifact, config, this.resolver(operation), this.runtimeDrivers),
|
|
3439
|
+
operation.cancellation,
|
|
3440
|
+
deadline
|
|
3441
|
+
]);
|
|
3442
|
+
} finally {
|
|
3443
|
+
if (timer) clearTimeout(timer);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
async prepareInteractiveWithDeadline(operation, artifact, config) {
|
|
3447
|
+
const timeoutMs = runtimePreparationTimeoutMs(artifact);
|
|
3448
|
+
let timer;
|
|
3449
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
3450
|
+
timer = setTimeout(() => {
|
|
3451
|
+
const error = /* @__PURE__ */ new Error(`Server interactive runtime preparation exceeded ${timeoutMs} ms.`);
|
|
3452
|
+
operation.superseded = true;
|
|
3453
|
+
this.abortPreparationStages(error, operation);
|
|
3454
|
+
reject(error);
|
|
3455
|
+
}, timeoutMs);
|
|
3456
|
+
});
|
|
3457
|
+
try {
|
|
3458
|
+
return await Promise.race([
|
|
3459
|
+
prepareArtifactInteraction(artifact, config, this.resolver(operation), this.runtimeDrivers),
|
|
3460
|
+
operation.cancellation,
|
|
3461
|
+
deadline
|
|
3462
|
+
]);
|
|
3463
|
+
} finally {
|
|
3464
|
+
if (timer) clearTimeout(timer);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
async prepareTrustedWithDeadline(operation, program, config) {
|
|
3468
|
+
let timer;
|
|
3469
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
3470
|
+
timer = setTimeout(() => {
|
|
3471
|
+
const error = /* @__PURE__ */ new Error(`Trusted judge runtime preparation exceeded ${DEFAULT_RUNTIME_PREPARATION_TIMEOUT_MS} ms.`);
|
|
3472
|
+
operation.superseded = true;
|
|
3473
|
+
this.abortPreparationStages(error, operation);
|
|
3474
|
+
reject(error);
|
|
3475
|
+
}, DEFAULT_RUNTIME_PREPARATION_TIMEOUT_MS);
|
|
3476
|
+
});
|
|
3477
|
+
try {
|
|
3478
|
+
return await Promise.race([
|
|
3479
|
+
Promise.resolve().then(() => prepareTrustedJudgeRun(program, config)),
|
|
3480
|
+
operation.cancellation,
|
|
3481
|
+
deadline
|
|
3482
|
+
]);
|
|
3483
|
+
} finally {
|
|
3484
|
+
if (timer) clearTimeout(timer);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
async loadQuickJsForOperation(operation) {
|
|
3488
|
+
this.assertCurrent(operation, "Server execution was cancelled before QuickJS was loaded.");
|
|
3489
|
+
const bytes = await this.loadQuickJs();
|
|
3490
|
+
this.assertCurrent(operation, "Server execution was cancelled while QuickJS was loaded.");
|
|
3491
|
+
return bytes;
|
|
3492
|
+
}
|
|
3493
|
+
async loadQuickJs() {
|
|
3494
|
+
return this.loadCompressedToolchainAsset(QUICKJS_ASSET_PATH, QUICKJS_ASSET_SHA256);
|
|
3495
|
+
}
|
|
3496
|
+
packageCommand(operation, packageSpecifier, command) {
|
|
3497
|
+
this.assertPinnedPackageCommand(packageSpecifier, command);
|
|
3498
|
+
this.assertCurrent(operation, "Server execution was cancelled before its package command was loaded.");
|
|
3499
|
+
const identity = `${packageSpecifier}\n${command}`;
|
|
3500
|
+
let pending = this.packageCommands.get(identity);
|
|
3501
|
+
if (!pending) {
|
|
3502
|
+
pending = this.runPackageStage(operation, {
|
|
3503
|
+
operation: "command-binary",
|
|
3504
|
+
packageSpecifier,
|
|
3505
|
+
command
|
|
3506
|
+
}).then((result) => {
|
|
3507
|
+
if (result.operation !== "command-binary") throw new Error(`Runner stage returned '${result.operation}' for a command-binary request.`);
|
|
3508
|
+
const verifiedBytes = new Uint8Array(result.bytes.byteLength);
|
|
3509
|
+
verifiedBytes.set(result.bytes);
|
|
3510
|
+
if (!WebAssembly.validate(verifiedBytes)) throw new Error(`Package '${packageSpecifier}' command '${command}' returned invalid WebAssembly.`);
|
|
3511
|
+
this.assertCurrent(operation, "Server execution was cancelled while its package command was loaded.");
|
|
3512
|
+
return verifiedBytes;
|
|
3513
|
+
}).catch((error) => {
|
|
3514
|
+
if (this.packageCommands.get(identity) === pending) this.packageCommands.delete(identity);
|
|
3515
|
+
throw error;
|
|
3516
|
+
});
|
|
3517
|
+
this.packageCommands.set(identity, pending);
|
|
3518
|
+
}
|
|
3519
|
+
return operation.track(pending.then((bytes) => bytes.slice()));
|
|
3520
|
+
}
|
|
3521
|
+
assertPinnedPackageCommand(packageSpecifier, command) {
|
|
3522
|
+
if (packageSpecifier !== PYTHON_PACKAGE || command !== "python") throw new Error(`No pinned WASM-OJ runtime command is declared for '${packageSpecifier}:${command}'.`);
|
|
3523
|
+
}
|
|
3524
|
+
async loadCompressedToolchainAsset(assetPath, compressedSha256, expandedSha256) {
|
|
3525
|
+
const file = serverToolchainAssetFile(this.toolchains, assetPath);
|
|
3526
|
+
const compressed = await readFile(file);
|
|
3527
|
+
if (!this.verifiedToolchain) this.verifyDigest(file, compressed, compressedSha256);
|
|
3528
|
+
const expandedBuffer = gunzipSync(compressed);
|
|
3529
|
+
const expanded = new Uint8Array(expandedBuffer.buffer, expandedBuffer.byteOffset, expandedBuffer.byteLength);
|
|
3530
|
+
if (expandedSha256 && !this.verifiedToolchain) this.verifyDigest(file, expanded, expandedSha256);
|
|
3531
|
+
return expanded;
|
|
3532
|
+
}
|
|
3533
|
+
verifyDigest(file, bytes, expected) {
|
|
3534
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
3535
|
+
if (actual !== expected) throw new Error(`Pinned toolchain asset '${file}' has digest ${actual}; expected ${expected}.`);
|
|
3536
|
+
}
|
|
3537
|
+
packageFileSystem(operation, request) {
|
|
3538
|
+
this.assertPinnedPackageCommand(request.packageSpecifier, request.command);
|
|
3539
|
+
this.assertCurrent(operation, "Server execution was cancelled before its runtime files were loaded.");
|
|
3540
|
+
const identity = `${request.packageSpecifier}\n${request.command}\n${request.cacheKey}\n${request.expectedSha256}`;
|
|
3541
|
+
let pending = this.packageFileSystems.get(identity);
|
|
3542
|
+
if (!pending) {
|
|
3543
|
+
pending = this.loadOrExportPackageFileSystem(operation, identity, request).catch((error) => {
|
|
3544
|
+
if (this.packageFileSystems.get(identity) === pending) this.packageFileSystems.delete(identity);
|
|
3545
|
+
throw error;
|
|
3546
|
+
});
|
|
3547
|
+
this.packageFileSystems.set(identity, pending);
|
|
3548
|
+
}
|
|
3549
|
+
return operation.track(pending.then(cloneRuntimeFiles));
|
|
3550
|
+
}
|
|
3551
|
+
async loadOrExportPackageFileSystem(operation, identity, request) {
|
|
3552
|
+
this.assertCurrent(operation, "Server execution was cancelled before its runtime cache was read.");
|
|
3553
|
+
const digest = createHash("sha256").update(identity).digest("hex");
|
|
3554
|
+
const cachePath = path.join(this.runtimeCacheDirectory(), `${digest}.wasmojfs`);
|
|
3555
|
+
const cachedArchive = await this.readRuntimeCacheArchive(cachePath);
|
|
3556
|
+
this.assertCurrent(operation, "Server execution was cancelled while its runtime cache was read.");
|
|
3557
|
+
if (cachedArchive) try {
|
|
3558
|
+
return await verifyAndDecodeRuntimeFiles(cachedArchive, request.expectedSha256);
|
|
3559
|
+
} catch (error) {
|
|
3560
|
+
this.reportRuntimeCacheIssue("Ignoring an invalid runtime cache archive", error);
|
|
3561
|
+
await this.removeInvalidRuntimeCacheArchive(cachePath);
|
|
3562
|
+
this.assertCurrent(operation, "Server execution was cancelled while invalid runtime cache data was removed.");
|
|
3563
|
+
}
|
|
3564
|
+
const result = await this.runPackageStage(operation, {
|
|
3565
|
+
operation: "runtime-files",
|
|
3566
|
+
packageSpecifier: request.packageSpecifier,
|
|
3567
|
+
command: request.command,
|
|
3568
|
+
args: [...request.args]
|
|
3569
|
+
});
|
|
3570
|
+
if (result.operation !== "runtime-files") throw new Error(`Runner stage returned '${result.operation}' for a runtime-files request.`);
|
|
3571
|
+
this.assertCurrent(operation, "Server execution was cancelled while its runtime files were exported.");
|
|
3572
|
+
const archive = result.bytes.slice();
|
|
3573
|
+
const files = await verifyAndDecodeRuntimeFiles(archive, request.expectedSha256);
|
|
3574
|
+
this.assertCurrent(operation, "Server execution was cancelled while its runtime files were verified.");
|
|
3575
|
+
await this.persistVerifiedRuntimeCacheArchive(cachePath, archive);
|
|
3576
|
+
this.assertCurrent(operation, "Server execution was cancelled while its runtime files were cached.");
|
|
3577
|
+
return files;
|
|
3578
|
+
}
|
|
3579
|
+
async runPackageStage(operation, request) {
|
|
3580
|
+
this.assertCurrent(operation, "Server execution was cancelled before its runtime stage started.");
|
|
3581
|
+
const transportDirectory = await mkdtemp(path.join(os.tmpdir(), "wasm-oj-runner-response-"));
|
|
3582
|
+
const responsePath = path.join(transportDirectory, "response.v8");
|
|
3583
|
+
try {
|
|
3584
|
+
this.assertCurrent(operation, "Server execution was cancelled before its runtime stage spawned.");
|
|
3585
|
+
return await new Promise((resolve, reject) => {
|
|
3586
|
+
const script = serverStageScript(this.stageDirectory, SERVER_RUNNER_STAGE_SCRIPT);
|
|
3587
|
+
const child = spawn(process.execPath, [
|
|
3588
|
+
"--experimental-strip-types",
|
|
3589
|
+
"--disable-warning=ExperimentalWarning",
|
|
3590
|
+
script
|
|
3591
|
+
], {
|
|
3592
|
+
stdio: [
|
|
3593
|
+
"pipe",
|
|
3594
|
+
"pipe",
|
|
3595
|
+
"pipe"
|
|
3596
|
+
],
|
|
3597
|
+
env: {
|
|
3598
|
+
...process.env,
|
|
3599
|
+
WASM_OJ_RUNNER_STAGE_RESPONSE: responsePath
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
const stdout = [];
|
|
3603
|
+
const stderr = [];
|
|
3604
|
+
let stdoutBytes = 0;
|
|
3605
|
+
let stderrBytes = 0;
|
|
3606
|
+
let settled = false;
|
|
3607
|
+
const cleanup = (ignoreLateChildError) => {
|
|
3608
|
+
child.off("close", onClose);
|
|
3609
|
+
child.off("error", onChildError);
|
|
3610
|
+
child.stdin.off("error", onStdinError);
|
|
3611
|
+
child.stdout.off("data", onStdout);
|
|
3612
|
+
child.stderr.off("data", onStderr);
|
|
3613
|
+
this.activePreparationStages.delete(active);
|
|
3614
|
+
if (ignoreLateChildError) child.once("error", () => void 0);
|
|
3615
|
+
};
|
|
3616
|
+
const fail = (error, kill) => {
|
|
3617
|
+
if (settled) return;
|
|
3618
|
+
settled = true;
|
|
3619
|
+
if (kill) try {
|
|
3620
|
+
child.kill("SIGKILL");
|
|
3621
|
+
} catch {}
|
|
3622
|
+
cleanup(kill);
|
|
3623
|
+
reject(error);
|
|
3624
|
+
};
|
|
3625
|
+
const succeed = (result) => {
|
|
3626
|
+
if (settled) return;
|
|
3627
|
+
settled = true;
|
|
3628
|
+
cleanup(false);
|
|
3629
|
+
resolve(result);
|
|
3630
|
+
};
|
|
3631
|
+
const capture = (chunks, chunk, currentBytes, stream) => {
|
|
3632
|
+
const nextBytes = currentBytes + chunk.byteLength;
|
|
3633
|
+
if (nextBytes > MAX_RUNNER_STAGE_DIAGNOSTIC_BYTES) {
|
|
3634
|
+
fail(/* @__PURE__ */ new Error(`The isolated server runtime stage exceeded its ${MAX_RUNNER_STAGE_DIAGNOSTIC_BYTES}-byte ${stream} limit.`), true);
|
|
3635
|
+
return currentBytes;
|
|
3636
|
+
}
|
|
3637
|
+
chunks.push(chunk);
|
|
3638
|
+
return nextBytes;
|
|
3639
|
+
};
|
|
3640
|
+
const onStdout = (chunk) => {
|
|
3641
|
+
stdoutBytes = capture(stdout, chunk, stdoutBytes, "stdout");
|
|
3642
|
+
};
|
|
3643
|
+
const onStderr = (chunk) => {
|
|
3644
|
+
stderrBytes = capture(stderr, chunk, stderrBytes, "stderr");
|
|
3645
|
+
};
|
|
3646
|
+
const onChildError = (error) => fail(error, true);
|
|
3647
|
+
const onStdinError = (error) => fail(error, true);
|
|
3648
|
+
const onClose = (code, signal) => {
|
|
3649
|
+
(async () => {
|
|
3650
|
+
try {
|
|
3651
|
+
const response = await readRunnerStageResponse(responsePath, request.operation);
|
|
3652
|
+
this.assertCurrent(operation, "Server execution was cancelled while its runtime stage completed.");
|
|
3653
|
+
if (code !== 0 || signal) throw new Error(runnerStageDiagnostic(stdout, stderr, `The isolated server runtime stage exited with ${signal ?? `code ${String(code)}`}.`));
|
|
3654
|
+
succeed(response);
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
fail(new Error(runnerStageDiagnostic(stdout, stderr, error instanceof Error ? error.message : String(error)), { cause: error }), false);
|
|
3657
|
+
}
|
|
3658
|
+
})();
|
|
3659
|
+
};
|
|
3660
|
+
const active = {
|
|
3661
|
+
operation,
|
|
3662
|
+
abort: (error) => fail(error, true)
|
|
3663
|
+
};
|
|
3664
|
+
this.activePreparationStages.add(active);
|
|
3665
|
+
child.stdout.on("data", onStdout);
|
|
3666
|
+
child.stderr.on("data", onStderr);
|
|
3667
|
+
child.on("error", onChildError);
|
|
3668
|
+
child.stdin.on("error", onStdinError);
|
|
3669
|
+
child.on("close", onClose);
|
|
3670
|
+
child.stdin.end(JSON.stringify({
|
|
3671
|
+
toolchainAsset: serverToolchainAssetFile(this.toolchains, PYTHON_PACKAGE_ASSET_PATH),
|
|
3672
|
+
verifiedToolchain: this.verifiedToolchain,
|
|
3673
|
+
request
|
|
3674
|
+
}));
|
|
3675
|
+
});
|
|
3676
|
+
} finally {
|
|
3677
|
+
await rm(transportDirectory, {
|
|
3678
|
+
recursive: true,
|
|
3679
|
+
force: true
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
async readRuntimeCacheArchive(cachePath) {
|
|
3684
|
+
try {
|
|
3685
|
+
const status = await lstat(cachePath);
|
|
3686
|
+
if (!status.isFile()) {
|
|
3687
|
+
this.progress({
|
|
3688
|
+
phase: "restoring-cache",
|
|
3689
|
+
label: `Ignoring non-regular runtime cache entry '${cachePath}'.`
|
|
3690
|
+
});
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (status.size > MAX_RUNTIME_CACHE_ARCHIVE_BYTES) {
|
|
3694
|
+
this.progress({
|
|
3695
|
+
phase: "restoring-cache",
|
|
3696
|
+
label: `Removing oversized runtime cache archive '${cachePath}' (${status.size} bytes).`
|
|
3697
|
+
});
|
|
3698
|
+
await this.removeInvalidRuntimeCacheArchive(cachePath);
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
return new Uint8Array(await readBoundedRegularFile(cachePath, MAX_RUNTIME_CACHE_ARCHIVE_BYTES));
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
if (isFileSystemError(error, "ENOENT")) return void 0;
|
|
3704
|
+
this.reportRuntimeCacheIssue("Unable to read the runtime cache archive", error);
|
|
3705
|
+
await this.removeInvalidRuntimeCacheArchive(cachePath);
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
async removeInvalidRuntimeCacheArchive(cachePath) {
|
|
3710
|
+
try {
|
|
3711
|
+
if ((await lstat(cachePath)).isFile()) await unlink(cachePath);
|
|
3712
|
+
} catch (error) {
|
|
3713
|
+
if (!isFileSystemError(error, "ENOENT")) this.reportRuntimeCacheIssue("Unable to remove the invalid runtime cache archive", error);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
async persistVerifiedRuntimeCacheArchive(cachePath, archive) {
|
|
3717
|
+
const temporary = `${cachePath}.${crypto.randomUUID()}.tmp`;
|
|
3718
|
+
try {
|
|
3719
|
+
await writeFile(temporary, archive, { flag: "wx" });
|
|
3720
|
+
await rename(temporary, cachePath);
|
|
3721
|
+
} catch (error) {
|
|
3722
|
+
this.reportRuntimeCacheIssue("Unable to persist the verified runtime cache archive", error);
|
|
3723
|
+
try {
|
|
3724
|
+
await unlink(temporary);
|
|
3725
|
+
} catch (cleanupError) {
|
|
3726
|
+
if (!isFileSystemError(cleanupError, "ENOENT")) this.reportRuntimeCacheIssue("Unable to remove a temporary runtime cache archive", cleanupError);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
reportRuntimeCacheIssue(action, error) {
|
|
3731
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3732
|
+
this.progress({
|
|
3733
|
+
phase: "restoring-cache",
|
|
3734
|
+
label: `${action}: ${detail}`
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
runNativeCore(request, config, started) {
|
|
3738
|
+
return new Promise((resolve, reject) => {
|
|
3739
|
+
const child = spawn(this.runtimeExecutable, [], { stdio: [
|
|
3740
|
+
"pipe",
|
|
3741
|
+
"pipe",
|
|
3742
|
+
"pipe"
|
|
3743
|
+
] });
|
|
3744
|
+
let settled = false;
|
|
3745
|
+
const cleanup = (ignoreLateChildError) => {
|
|
3746
|
+
clearTimeout(timer);
|
|
3747
|
+
child.off("close", onClose);
|
|
3748
|
+
child.off("error", onChildError);
|
|
3749
|
+
child.stdin.off("error", onStdinError);
|
|
3750
|
+
child.stdout.off("data", onStdout);
|
|
3751
|
+
child.stderr.off("data", onStderr);
|
|
3752
|
+
this.activeNativeRuns.delete(active);
|
|
3753
|
+
if (ignoreLateChildError) child.once("error", () => void 0);
|
|
3754
|
+
};
|
|
3755
|
+
const fail = (error, kill) => {
|
|
3756
|
+
if (settled) return;
|
|
3757
|
+
settled = true;
|
|
3758
|
+
if (kill) try {
|
|
3759
|
+
child.kill("SIGKILL");
|
|
3760
|
+
} catch {}
|
|
3761
|
+
cleanup(kill);
|
|
3762
|
+
reject(error);
|
|
3763
|
+
};
|
|
3764
|
+
const succeed = (result, kill) => {
|
|
3765
|
+
if (settled) return;
|
|
3766
|
+
settled = true;
|
|
3767
|
+
if (kill) try {
|
|
3768
|
+
child.kill("SIGKILL");
|
|
3769
|
+
} catch {}
|
|
3770
|
+
cleanup(kill);
|
|
3771
|
+
resolve(result);
|
|
3772
|
+
};
|
|
3773
|
+
const stdout = new BoundedByteCollector("Native runtime-core protocol stdout", nativeCoreResponseLimit(config.resources.outputLimitBytes), (error) => fail(error, true));
|
|
3774
|
+
const stderr = new BoundedByteCollector("Native runtime-core diagnostic stderr", MAX_NATIVE_CORE_DIAGNOSTIC_BYTES, (error) => fail(error, true));
|
|
3775
|
+
const onStdout = (chunk) => stdout.append(chunk);
|
|
3776
|
+
const onStderr = (chunk) => stderr.append(chunk);
|
|
3777
|
+
const onChildError = (error) => fail(error, true);
|
|
3778
|
+
const onStdinError = (error) => fail(error, true);
|
|
3779
|
+
const onClose = () => {
|
|
3780
|
+
const durationMs = performance.now() - started;
|
|
3781
|
+
try {
|
|
3782
|
+
const output = stdout.text();
|
|
3783
|
+
const response = JSON.parse(output);
|
|
3784
|
+
if (!response.ok || !response.result) {
|
|
3785
|
+
const error = response.error ?? {
|
|
3786
|
+
code: "RUNTIME_ERROR",
|
|
3787
|
+
message: stderr.text() || "Native runtime returned no result."
|
|
3788
|
+
};
|
|
3789
|
+
throw Object.assign(new Error(error.message), { code: error.code });
|
|
3790
|
+
}
|
|
3791
|
+
const trapMessage = response.result.trapMessage;
|
|
3792
|
+
if (trapMessage !== void 0 && trapMessage !== null && typeof trapMessage !== "string") throw new Error("Native runtime returned an invalid trap message.");
|
|
3793
|
+
succeed({
|
|
3794
|
+
code: response.result.code,
|
|
3795
|
+
stdout: Buffer.from(response.result.stdoutBase64, "base64").toString("utf8"),
|
|
3796
|
+
stderr: Buffer.from(response.result.stderrBase64, "base64").toString("utf8"),
|
|
3797
|
+
files: decodeNativeOutputFiles(response.result.filesBase64),
|
|
3798
|
+
durationMs,
|
|
3799
|
+
determinism: { ...config.determinism },
|
|
3800
|
+
resources: { ...config.resources },
|
|
3801
|
+
termination: response.result.termination,
|
|
3802
|
+
...typeof trapMessage === "string" ? { trapMessage } : {},
|
|
3803
|
+
metrics: normalizeExecutionMetrics(response.result.metrics, request.cost)
|
|
3804
|
+
}, false);
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
fail(error instanceof Error ? error : new Error(String(error)), false);
|
|
3807
|
+
}
|
|
3808
|
+
};
|
|
3809
|
+
const active = {
|
|
3810
|
+
child,
|
|
3811
|
+
abort: (error) => fail(error, true)
|
|
3812
|
+
};
|
|
3813
|
+
const timer = setTimeout(() => {
|
|
3814
|
+
succeed({
|
|
3815
|
+
code: 137,
|
|
3816
|
+
stdout: "",
|
|
3817
|
+
stderr: `Execution exceeded the ${config.resources.wallTimeLimitMs} ms wall deadline.`,
|
|
3818
|
+
files: {},
|
|
3819
|
+
durationMs: performance.now() - started,
|
|
3820
|
+
determinism: { ...config.determinism },
|
|
3821
|
+
resources: { ...config.resources },
|
|
3822
|
+
termination: "wall-time-limit",
|
|
3823
|
+
metrics: unavailableExecutionMetrics(request.cost, WEIGHTED_METER_MODEL)
|
|
3824
|
+
}, true);
|
|
3825
|
+
}, config.resources.wallTimeLimitMs);
|
|
3826
|
+
this.activeNativeRuns.add(active);
|
|
3827
|
+
child.stdout.on("data", onStdout);
|
|
3828
|
+
child.stderr.on("data", onStderr);
|
|
3829
|
+
child.on("error", onChildError);
|
|
3830
|
+
child.stdin.on("error", onStdinError);
|
|
3831
|
+
child.on("close", onClose);
|
|
3832
|
+
child.stdin.end(JSON.stringify(encodeNativeRequest(request)));
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
runNativeInteractive(contestant, interactor, config, started) {
|
|
3836
|
+
return new Promise((resolve, reject) => {
|
|
3837
|
+
const child = spawn(this.runtimeExecutable, [], { stdio: [
|
|
3838
|
+
"pipe",
|
|
3839
|
+
"pipe",
|
|
3840
|
+
"pipe"
|
|
3841
|
+
] });
|
|
3842
|
+
let settled = false;
|
|
3843
|
+
const cleanup = (ignoreLateChildError) => {
|
|
3844
|
+
clearTimeout(timer);
|
|
3845
|
+
child.off("close", onClose);
|
|
3846
|
+
child.off("error", onChildError);
|
|
3847
|
+
child.stdin.off("error", onStdinError);
|
|
3848
|
+
child.stdout.off("data", onStdout);
|
|
3849
|
+
child.stderr.off("data", onStderr);
|
|
3850
|
+
this.activeNativeRuns.delete(active);
|
|
3851
|
+
if (ignoreLateChildError) child.once("error", () => void 0);
|
|
3852
|
+
};
|
|
3853
|
+
const fail = (error, kill) => {
|
|
3854
|
+
if (settled) return;
|
|
3855
|
+
settled = true;
|
|
3856
|
+
if (kill) child.kill("SIGKILL");
|
|
3857
|
+
cleanup(kill);
|
|
3858
|
+
reject(error);
|
|
3859
|
+
};
|
|
3860
|
+
const succeed = (result, kill) => {
|
|
3861
|
+
if (settled) return;
|
|
3862
|
+
settled = true;
|
|
3863
|
+
if (kill) child.kill("SIGKILL");
|
|
3864
|
+
cleanup(kill);
|
|
3865
|
+
resolve(result);
|
|
3866
|
+
};
|
|
3867
|
+
const stdout = new BoundedByteCollector("Native interactive protocol stdout", nativeCoreResponseLimit(contestant.resources.outputLimitBytes + interactor.resources.outputLimitBytes), (error) => fail(error, true));
|
|
3868
|
+
const stderr = new BoundedByteCollector("Native interactive diagnostic stderr", MAX_NATIVE_CORE_DIAGNOSTIC_BYTES, (error) => fail(error, true));
|
|
3869
|
+
const onStdout = (chunk) => stdout.append(chunk);
|
|
3870
|
+
const onStderr = (chunk) => stderr.append(chunk);
|
|
3871
|
+
const onChildError = (error) => fail(error, true);
|
|
3872
|
+
const onStdinError = (error) => fail(error, true);
|
|
3873
|
+
const onClose = () => {
|
|
3874
|
+
try {
|
|
3875
|
+
const response = JSON.parse(stdout.text());
|
|
3876
|
+
if (!response.ok || !response.result) {
|
|
3877
|
+
const error = response.error ?? {
|
|
3878
|
+
code: "RUNTIME_ERROR",
|
|
3879
|
+
message: stderr.text() || "Native interactive runtime returned no result."
|
|
3880
|
+
};
|
|
3881
|
+
throw Object.assign(new Error(error.message), { code: error.code });
|
|
3882
|
+
}
|
|
3883
|
+
succeed({
|
|
3884
|
+
contestant: nativeInteractiveProcess(response.result.contestant, contestant),
|
|
3885
|
+
interactor: nativeInteractiveProcess(response.result.interactor, interactor),
|
|
3886
|
+
contestantToInteractor: Buffer.from(response.result.contestantToInteractorBase64, "base64").toString("utf8"),
|
|
3887
|
+
interactorToContestant: Buffer.from(response.result.interactorToContestantBase64, "base64").toString("utf8"),
|
|
3888
|
+
durationMs: performance.now() - started,
|
|
3889
|
+
determinism: { ...config.determinism }
|
|
3890
|
+
}, false);
|
|
3891
|
+
} catch (error) {
|
|
3892
|
+
fail(error instanceof Error ? error : new Error(String(error)), false);
|
|
3893
|
+
}
|
|
3894
|
+
};
|
|
3895
|
+
const active = {
|
|
3896
|
+
child,
|
|
3897
|
+
abort: (error) => fail(error, true)
|
|
3898
|
+
};
|
|
3899
|
+
const wallTimeLimitMs = Math.min(config.contestant.resources.wallTimeLimitMs, config.interactor.resources.wallTimeLimitMs);
|
|
3900
|
+
const timer = setTimeout(() => {
|
|
3901
|
+
const message = `Interactive execution exceeded the ${wallTimeLimitMs} ms wall deadline.`;
|
|
3902
|
+
succeed({
|
|
3903
|
+
contestant: {
|
|
3904
|
+
code: 137,
|
|
3905
|
+
stderr: message,
|
|
3906
|
+
termination: "wall-time-limit",
|
|
3907
|
+
metrics: unavailableExecutionMetrics(contestant.cost, WEIGHTED_METER_MODEL)
|
|
3908
|
+
},
|
|
3909
|
+
interactor: {
|
|
3910
|
+
code: 137,
|
|
3911
|
+
stderr: message,
|
|
3912
|
+
termination: "wall-time-limit",
|
|
3913
|
+
metrics: unavailableExecutionMetrics(interactor.cost, WEIGHTED_METER_MODEL)
|
|
3914
|
+
},
|
|
3915
|
+
contestantToInteractor: "",
|
|
3916
|
+
interactorToContestant: "",
|
|
3917
|
+
durationMs: performance.now() - started,
|
|
3918
|
+
determinism: { ...config.determinism }
|
|
3919
|
+
}, true);
|
|
3920
|
+
}, wallTimeLimitMs);
|
|
3921
|
+
this.activeNativeRuns.add(active);
|
|
3922
|
+
child.stdout.on("data", onStdout);
|
|
3923
|
+
child.stderr.on("data", onStderr);
|
|
3924
|
+
child.on("error", onChildError);
|
|
3925
|
+
child.stdin.on("error", onStdinError);
|
|
3926
|
+
child.on("close", onClose);
|
|
3927
|
+
child.stdin.end(JSON.stringify(encodeNativeInteractiveRequest(contestant, interactor, config)));
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
progress(progress) {
|
|
3931
|
+
for (const listener of this.progressListeners) listener(progress);
|
|
3932
|
+
}
|
|
3933
|
+
stream(stream, chunk) {
|
|
3934
|
+
for (const listener of this.streamListeners) listener(stream, chunk);
|
|
3935
|
+
}
|
|
3936
|
+
assertCurrent(operation, message) {
|
|
3937
|
+
this.assertActive();
|
|
3938
|
+
if (operation.superseded || operation.generation !== this.generation) throw new Error(message);
|
|
3939
|
+
}
|
|
3940
|
+
abortPreparationStages(error, operation) {
|
|
3941
|
+
for (const active of [...this.activePreparationStages]) if (!operation || active.operation === operation) active.abort(error);
|
|
3942
|
+
}
|
|
3943
|
+
terminateActiveNativeRuns(error) {
|
|
3944
|
+
for (const active of [...this.activeNativeRuns]) active.abort(error);
|
|
3945
|
+
}
|
|
3946
|
+
assertActive() {
|
|
3947
|
+
if (this.disposed) throw new Error("ServerRunner is disposed.");
|
|
3948
|
+
}
|
|
3949
|
+
assertArtifactToolchain(artifact) {
|
|
3950
|
+
assertServerToolchainProfile(this.toolchains, artifact.language, artifact.target, artifact.optimization);
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
function createServerRunOperation(generation) {
|
|
3954
|
+
let rejectCancellation;
|
|
3955
|
+
let resolveCompletion;
|
|
3956
|
+
let cancelled = false;
|
|
3957
|
+
let completed = false;
|
|
3958
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
3959
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
3960
|
+
rejectCancellation = reject;
|
|
3961
|
+
});
|
|
3962
|
+
cancellation.catch(() => void 0);
|
|
3963
|
+
return {
|
|
3964
|
+
generation,
|
|
3965
|
+
superseded: false,
|
|
3966
|
+
cancellation,
|
|
3967
|
+
cancel(error) {
|
|
3968
|
+
if (cancelled) return;
|
|
3969
|
+
cancelled = true;
|
|
3970
|
+
rejectCancellation(error);
|
|
3971
|
+
},
|
|
3972
|
+
track(task) {
|
|
3973
|
+
const settled = task.then(() => void 0, () => void 0);
|
|
3974
|
+
tasks.add(settled);
|
|
3975
|
+
settled.then(() => tasks.delete(settled));
|
|
3976
|
+
return task;
|
|
3977
|
+
},
|
|
3978
|
+
async quiesce() {
|
|
3979
|
+
while (tasks.size > 0) await Promise.all([...tasks]);
|
|
3980
|
+
},
|
|
3981
|
+
completion: new Promise((resolve) => {
|
|
3982
|
+
resolveCompletion = resolve;
|
|
3983
|
+
}),
|
|
3984
|
+
complete() {
|
|
3985
|
+
if (completed) return;
|
|
3986
|
+
completed = true;
|
|
3987
|
+
resolveCompletion();
|
|
3988
|
+
}
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
async function readRunnerStageResponse(responsePath, expectedOperation) {
|
|
3992
|
+
const encoded = await readBoundedRegularFile(responsePath, MAX_RUNNER_STAGE_RESPONSE_BYTES);
|
|
3993
|
+
if (encoded.byteLength === 0) throw new Error("The isolated server runtime stage returned an empty response.");
|
|
3994
|
+
const response = deserialize(encoded);
|
|
3995
|
+
if (!isRecord(response) || typeof response.ok !== "boolean") throw new Error("The isolated server runtime stage returned an invalid response envelope.");
|
|
3996
|
+
if (!response.ok) {
|
|
3997
|
+
if (typeof response.error !== "string" || response.error.length === 0) throw new Error("The isolated server runtime stage failed without an error message.");
|
|
3998
|
+
throw new Error(response.error);
|
|
3999
|
+
}
|
|
4000
|
+
if (!isRecord(response.result)) throw new Error("The isolated server runtime stage returned no result.");
|
|
4001
|
+
const operation = response.result.operation;
|
|
4002
|
+
const bytes = response.result.bytes;
|
|
4003
|
+
if (operation !== expectedOperation) throw new Error(`The isolated server runtime stage returned '${String(operation)}' for '${expectedOperation}'.`);
|
|
4004
|
+
if (!(bytes instanceof Uint8Array)) throw new Error("The isolated server runtime stage returned non-binary result bytes.");
|
|
4005
|
+
return {
|
|
4006
|
+
operation: expectedOperation,
|
|
4007
|
+
bytes: bytes.slice()
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
function nativeCoreResponseLimit(outputLimitBytes) {
|
|
4011
|
+
return Math.ceil(outputLimitBytes / 3) * 4 + NATIVE_CORE_RESPONSE_OVERHEAD_BYTES;
|
|
4012
|
+
}
|
|
4013
|
+
function runnerStageDiagnostic(stdout, stderr, summary) {
|
|
4014
|
+
const stageDiagnostic = Buffer.concat(stderr).toString("utf8").trim() || Buffer.concat(stdout).toString("utf8").trim();
|
|
4015
|
+
return stageDiagnostic ? `${summary}\n${stageDiagnostic}` : summary;
|
|
4016
|
+
}
|
|
4017
|
+
function cloneRuntimeFiles(files) {
|
|
4018
|
+
return Object.fromEntries(Object.entries(files).map(([filePath, contents]) => [filePath, contents.slice()]));
|
|
4019
|
+
}
|
|
4020
|
+
function isRecord(value) {
|
|
4021
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4022
|
+
}
|
|
4023
|
+
function encodeNativeRequest(request) {
|
|
4024
|
+
return {
|
|
4025
|
+
schema: WASM_OJ_SCHEMAS.runRequest,
|
|
4026
|
+
wasmBase64: Buffer.from(request.wasm).toString("base64"),
|
|
4027
|
+
args: request.args,
|
|
4028
|
+
env: request.env,
|
|
4029
|
+
stdinBase64: Buffer.from(request.stdin).toString("base64"),
|
|
4030
|
+
filesBase64: Object.fromEntries(Object.entries(request.files).map(([filePath, contents]) => [filePath, Buffer.from(contents).toString("base64")])),
|
|
4031
|
+
outputPaths: request.outputPaths,
|
|
4032
|
+
cwd: request.cwd,
|
|
4033
|
+
startupEntropyBytes: request.startupEntropyBytes,
|
|
4034
|
+
determinism: request.determinism,
|
|
4035
|
+
resources: request.resources
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
function interactiveRunConfig(program, determinism) {
|
|
4039
|
+
return {
|
|
4040
|
+
args: [...program.args],
|
|
4041
|
+
stdin: "",
|
|
4042
|
+
env: { ...program.env },
|
|
4043
|
+
files: Object.fromEntries(Object.entries(program.files ?? {}).map(([path, contents]) => [path, contents.slice()])),
|
|
4044
|
+
outputPaths: [],
|
|
4045
|
+
...program.cwd === void 0 ? {} : { cwd: program.cwd },
|
|
4046
|
+
determinism: { ...determinism },
|
|
4047
|
+
resources: { ...program.resources }
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
function encodeNativeInteractiveRequest(contestant, interactor, config) {
|
|
4051
|
+
return {
|
|
4052
|
+
schema: WASM_OJ_SCHEMAS.interactiveRequest,
|
|
4053
|
+
contestant: encodeNativeInteractiveProgram(contestant),
|
|
4054
|
+
interactor: encodeNativeInteractiveProgram(interactor),
|
|
4055
|
+
determinism: config.determinism
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
function encodeNativeInteractiveProgram(request) {
|
|
4059
|
+
return {
|
|
4060
|
+
wasmBase64: Buffer.from(request.wasm).toString("base64"),
|
|
4061
|
+
args: request.args,
|
|
4062
|
+
env: request.env,
|
|
4063
|
+
filesBase64: Object.fromEntries(Object.entries(request.files).map(([filePath, contents]) => [filePath, Buffer.from(contents).toString("base64")])),
|
|
4064
|
+
cwd: request.cwd,
|
|
4065
|
+
startupEntropyBytes: request.startupEntropyBytes,
|
|
4066
|
+
resources: request.resources
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
function nativeInteractiveProcess(result, prepared) {
|
|
4070
|
+
const metrics = normalizeExecutionMetrics({
|
|
4071
|
+
cost: result.metrics.cost,
|
|
4072
|
+
costModel: WEIGHTED_METER_MODEL,
|
|
4073
|
+
operations: result.metrics.operations,
|
|
4074
|
+
memoryBytes: 0,
|
|
4075
|
+
logicalTimeNs: result.metrics.logicalTimeNs,
|
|
4076
|
+
filesystemBytes: result.metrics.filesystemBytes,
|
|
4077
|
+
filesystemEntries: result.metrics.filesystemEntries,
|
|
4078
|
+
stdoutBytes: result.metrics.protocolBytes,
|
|
4079
|
+
stderrBytes: result.metrics.stderrBytes
|
|
4080
|
+
}, prepared.cost);
|
|
4081
|
+
return {
|
|
4082
|
+
code: result.code,
|
|
4083
|
+
stderr: Buffer.from(result.stderrBase64, "base64").toString("utf8"),
|
|
4084
|
+
termination: result.termination,
|
|
4085
|
+
metrics: {
|
|
4086
|
+
...metrics,
|
|
4087
|
+
memoryBytes: null
|
|
4088
|
+
}
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
function decodeNativeOutputFiles(value) {
|
|
4092
|
+
if (!isRecord(value) || Object.keys(value).length > 256) throw new Error("Native runtime returned an invalid output file record.");
|
|
4093
|
+
const files = {};
|
|
4094
|
+
for (const [path, encoded] of Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) {
|
|
4095
|
+
if (typeof encoded !== "string") throw new Error(`Native runtime output file '${path}' is not base64 text.`);
|
|
4096
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
4097
|
+
if (bytes.toString("base64") !== encoded) throw new Error(`Native runtime output file '${path}' has non-canonical base64.`);
|
|
4098
|
+
files[path] = new Uint8Array(bytes);
|
|
4099
|
+
}
|
|
4100
|
+
return files;
|
|
4101
|
+
}
|
|
4102
|
+
function assertCacheDirectoryIsNotFilesystemRoot(directory) {
|
|
4103
|
+
if (directory === path.parse(directory).root) throw new Error("ServerRunner cacheDirectory must not be a filesystem root.");
|
|
4104
|
+
}
|
|
4105
|
+
function isWasmOjRuntimeCacheFile(name) {
|
|
4106
|
+
return WASM_OJ_RUNTIME_CACHE_FILE.test(name) || WASM_OJ_RUNTIME_CACHE_TEMPORARY_FILE.test(name);
|
|
4107
|
+
}
|
|
4108
|
+
async function removeWasmOjRuntimeCacheFiles(directory) {
|
|
4109
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
4110
|
+
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
4111
|
+
for (const entry of entries) {
|
|
4112
|
+
if (!entry.isFile() || !isWasmOjRuntimeCacheFile(entry.name)) continue;
|
|
4113
|
+
const file = path.join(directory, entry.name);
|
|
4114
|
+
try {
|
|
4115
|
+
if (!(await lstat(file)).isFile()) continue;
|
|
4116
|
+
await unlink(file);
|
|
4117
|
+
} catch (error) {
|
|
4118
|
+
if (!isFileSystemError(error, "ENOENT")) throw error;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
function isFileSystemError(error, code) {
|
|
4123
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
4124
|
+
}
|
|
4125
|
+
//#endregion
|
|
4126
|
+
//#region src/dependencies/filesystem-cache.ts
|
|
4127
|
+
var SHA256 = /^[0-9a-f]{64}$/;
|
|
4128
|
+
var MAX_PAYLOAD_BYTES = 536870912;
|
|
4129
|
+
/** Atomic server-side content-addressed dependency cache. */
|
|
4130
|
+
var FileSystemDependencyCache = class {
|
|
4131
|
+
directory;
|
|
4132
|
+
initialized;
|
|
4133
|
+
constructor(directory) {
|
|
4134
|
+
if (!path.isAbsolute(directory)) throw new Error("Dependency cache directory must be absolute.");
|
|
4135
|
+
this.directory = directory;
|
|
4136
|
+
}
|
|
4137
|
+
async load(integritySha256) {
|
|
4138
|
+
requireDigest(integritySha256);
|
|
4139
|
+
await this.ready();
|
|
4140
|
+
const file = this.pathFor(integritySha256);
|
|
4141
|
+
let handle;
|
|
4142
|
+
try {
|
|
4143
|
+
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4144
|
+
} catch (error) {
|
|
4145
|
+
if (error.code === "ENOENT") return void 0;
|
|
4146
|
+
if (error.code === "ELOOP") {
|
|
4147
|
+
await rm(file, { force: true });
|
|
4148
|
+
throw new Error(`Cached dependency '${integritySha256}' must not be a symbolic link.`);
|
|
4149
|
+
}
|
|
4150
|
+
throw error;
|
|
4151
|
+
}
|
|
4152
|
+
try {
|
|
4153
|
+
const metadata = await handle.stat();
|
|
4154
|
+
if (!metadata.isFile() || metadata.size > MAX_PAYLOAD_BYTES) throw new Error(`Cached dependency '${integritySha256}' is not a bounded regular file.`);
|
|
4155
|
+
const payload = new Uint8Array(await handle.readFile());
|
|
4156
|
+
if (digest(payload) !== integritySha256) throw new Error(`Cached dependency '${integritySha256}' failed integrity verification.`);
|
|
4157
|
+
return payload;
|
|
4158
|
+
} catch (error) {
|
|
4159
|
+
await rm(file, { force: true });
|
|
4160
|
+
throw error;
|
|
4161
|
+
} finally {
|
|
4162
|
+
await handle.close();
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
async save(integritySha256, payload) {
|
|
4166
|
+
requireDigest(integritySha256);
|
|
4167
|
+
if (!(payload instanceof Uint8Array) || payload.byteLength > MAX_PAYLOAD_BYTES || digest(payload) !== integritySha256) throw new Error("Dependency cache payload digest mismatch or size limit exceeded.");
|
|
4168
|
+
await this.ready();
|
|
4169
|
+
const destination = this.pathFor(integritySha256);
|
|
4170
|
+
const temporary = path.join(this.directory, `${integritySha256}.${randomUUID()}.tmp`);
|
|
4171
|
+
try {
|
|
4172
|
+
await writeFile(temporary, payload, {
|
|
4173
|
+
flag: "wx",
|
|
4174
|
+
mode: 384
|
|
4175
|
+
});
|
|
4176
|
+
await rename(temporary, destination);
|
|
4177
|
+
} finally {
|
|
4178
|
+
await rm(temporary, { force: true });
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
async delete(integritySha256) {
|
|
4182
|
+
requireDigest(integritySha256);
|
|
4183
|
+
await this.ready();
|
|
4184
|
+
await rm(this.pathFor(integritySha256), { force: true });
|
|
4185
|
+
}
|
|
4186
|
+
async clear() {
|
|
4187
|
+
await rm(this.directory, {
|
|
4188
|
+
recursive: true,
|
|
4189
|
+
force: true
|
|
4190
|
+
});
|
|
4191
|
+
this.initialized = void 0;
|
|
4192
|
+
await this.ready();
|
|
4193
|
+
}
|
|
4194
|
+
ready() {
|
|
4195
|
+
this.initialized ??= mkdir(this.directory, {
|
|
4196
|
+
recursive: true,
|
|
4197
|
+
mode: 448
|
|
4198
|
+
}).then(async () => {
|
|
4199
|
+
const metadata = await lstat(this.directory);
|
|
4200
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("Dependency cache path must be a real directory, not a symbolic link.");
|
|
4201
|
+
});
|
|
4202
|
+
this.initialized.catch(() => {
|
|
4203
|
+
this.initialized = void 0;
|
|
4204
|
+
});
|
|
4205
|
+
return this.initialized;
|
|
4206
|
+
}
|
|
4207
|
+
pathFor(integritySha256) {
|
|
4208
|
+
return path.join(this.directory, `${integritySha256}.bin`);
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
function requireDigest(value) {
|
|
4212
|
+
if (!SHA256.test(value)) throw new Error("Dependency integrity must be lowercase SHA-256 hexadecimal.");
|
|
4213
|
+
}
|
|
4214
|
+
function digest(payload) {
|
|
4215
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
4216
|
+
}
|
|
4217
|
+
//#endregion
|
|
4218
|
+
//#region src/server/artifact-store.ts
|
|
4219
|
+
var MAX_SERIALIZED_ARTIFACT_BYTES = 536870912;
|
|
4220
|
+
/** Atomic, content-addressed artifact storage for the Node/server host. */
|
|
4221
|
+
var FileSystemArtifactStore = class {
|
|
4222
|
+
directory;
|
|
4223
|
+
initialized;
|
|
4224
|
+
constructor(directory) {
|
|
4225
|
+
this.directory = directory;
|
|
4226
|
+
if (!path.isAbsolute(directory)) throw new Error("Artifact cache directory must be absolute.");
|
|
4227
|
+
}
|
|
4228
|
+
async load(cacheKey) {
|
|
4229
|
+
assertCompilerCacheKey(cacheKey);
|
|
4230
|
+
await this.ready();
|
|
4231
|
+
const file = this.pathFor(cacheKey);
|
|
4232
|
+
let handle;
|
|
4233
|
+
try {
|
|
4234
|
+
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4235
|
+
} catch (error) {
|
|
4236
|
+
if (error.code === "ENOENT") return void 0;
|
|
4237
|
+
if (error.code === "ELOOP") {
|
|
4238
|
+
await rm(file, { force: true });
|
|
4239
|
+
throw new Error(`Cached artifact '${cacheKey}' must not be a symbolic link.`);
|
|
4240
|
+
}
|
|
4241
|
+
throw error;
|
|
4242
|
+
}
|
|
4243
|
+
try {
|
|
4244
|
+
const metadata = await handle.stat();
|
|
4245
|
+
if (!metadata.isFile() || metadata.size > MAX_SERIALIZED_ARTIFACT_BYTES) throw new Error(`Cached artifact '${cacheKey}' is not a bounded regular file.`);
|
|
4246
|
+
const artifact = deserialize(await handle.readFile());
|
|
4247
|
+
assertValidBuildArtifact(artifact);
|
|
4248
|
+
if (artifact.cacheKey !== cacheKey) throw new Error("Cached artifact build identity does not match its key.");
|
|
4249
|
+
return artifact;
|
|
4250
|
+
} catch (error) {
|
|
4251
|
+
await rm(file, { force: true });
|
|
4252
|
+
throw error;
|
|
4253
|
+
} finally {
|
|
4254
|
+
await handle.close();
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
async save(artifact) {
|
|
4258
|
+
assertValidBuildArtifact(artifact);
|
|
4259
|
+
await this.ready();
|
|
4260
|
+
const encoded = serialize(artifact);
|
|
4261
|
+
if (encoded.byteLength > MAX_SERIALIZED_ARTIFACT_BYTES) throw new RangeError(`Serialized artifact exceeds ${MAX_SERIALIZED_ARTIFACT_BYTES} bytes.`);
|
|
4262
|
+
const temporary = path.join(this.directory, `${cacheFileName(artifact.cacheKey)}.${randomUUID()}.tmp`);
|
|
4263
|
+
try {
|
|
4264
|
+
await writeFile(temporary, encoded, {
|
|
4265
|
+
flag: "wx",
|
|
4266
|
+
mode: 384
|
|
4267
|
+
});
|
|
4268
|
+
await rename(temporary, this.pathFor(artifact.cacheKey));
|
|
4269
|
+
} finally {
|
|
4270
|
+
await rm(temporary, { force: true });
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
async delete(cacheKey) {
|
|
4274
|
+
assertCompilerCacheKey(cacheKey);
|
|
4275
|
+
await this.ready();
|
|
4276
|
+
await rm(this.pathFor(cacheKey), { force: true });
|
|
4277
|
+
}
|
|
4278
|
+
async clear() {
|
|
4279
|
+
await rm(this.directory, {
|
|
4280
|
+
recursive: true,
|
|
4281
|
+
force: true
|
|
4282
|
+
});
|
|
4283
|
+
this.initialized = void 0;
|
|
4284
|
+
await this.ready();
|
|
4285
|
+
}
|
|
4286
|
+
ready() {
|
|
4287
|
+
this.initialized ??= mkdir(this.directory, {
|
|
4288
|
+
recursive: true,
|
|
4289
|
+
mode: 448
|
|
4290
|
+
}).then(async () => {
|
|
4291
|
+
const metadata = await lstat(this.directory);
|
|
4292
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("Artifact cache path must be a real directory, not a symbolic link.");
|
|
4293
|
+
});
|
|
4294
|
+
this.initialized.catch(() => {
|
|
4295
|
+
this.initialized = void 0;
|
|
4296
|
+
});
|
|
4297
|
+
return this.initialized;
|
|
4298
|
+
}
|
|
4299
|
+
pathFor(cacheKey) {
|
|
4300
|
+
return path.join(this.directory, `${cacheFileName(cacheKey)}.wasm-oj-artifact`);
|
|
4301
|
+
}
|
|
4302
|
+
};
|
|
4303
|
+
function cacheFileName(cacheKey) {
|
|
4304
|
+
return createHash("sha256").update(cacheKey).digest("hex");
|
|
4305
|
+
}
|
|
4306
|
+
//#endregion
|
|
4307
|
+
//#region src/server/factory.ts
|
|
4308
|
+
/** Resolve an explicitly provisioned WASM-OJ distribution without performing I/O. */
|
|
4309
|
+
function resolveServerPaths(options) {
|
|
4310
|
+
if (typeof options?.runtimeDirectory !== "string" || !options.runtimeDirectory.trim()) throw new Error("ServerEngineOptions.runtimeDirectory is required.");
|
|
4311
|
+
const runtimeDirectory = path.resolve(options.runtimeDirectory);
|
|
4312
|
+
const toolchains = snapshotServerToolchainSources(options.toolchains);
|
|
4313
|
+
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
4314
|
+
return Object.freeze({
|
|
4315
|
+
compilerExecutable: path.join(runtimeDirectory, `wasm-oj-compiler${suffix}`),
|
|
4316
|
+
runtimeExecutable: path.join(runtimeDirectory, `wasm-oj-runner${suffix}`),
|
|
4317
|
+
toolchains,
|
|
4318
|
+
cacheDirectory: path.resolve(options.cacheDirectory ?? path.join(process.cwd(), ".wasm-oj"))
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
/** Verify the explicit local distribution and construct one ready server engine. */
|
|
4322
|
+
async function createServerEngine(options) {
|
|
4323
|
+
const paths = resolveServerPaths(options);
|
|
4324
|
+
try {
|
|
4325
|
+
if (path.parse(paths.cacheDirectory).root === paths.cacheDirectory) throw new Error("WASM-OJ server cache directory cannot be a filesystem root.");
|
|
4326
|
+
if (options.verifiedDistribution) assertVerifiedServerDistribution(options.verifiedDistribution, paths, paths.toolchains);
|
|
4327
|
+
else {
|
|
4328
|
+
await Promise.all([verifyExecutable(paths.compilerExecutable, "WASM-OJ compiler"), verifyExecutable(paths.runtimeExecutable, "WASM-OJ runner")]);
|
|
4329
|
+
await verifyToolchainSources(paths.toolchains);
|
|
4330
|
+
}
|
|
4331
|
+
await mkdir(paths.cacheDirectory, {
|
|
4332
|
+
recursive: true,
|
|
4333
|
+
mode: 448
|
|
4334
|
+
});
|
|
4335
|
+
const compiler = new ServerCompiler({
|
|
4336
|
+
compilerExecutable: paths.compilerExecutable,
|
|
4337
|
+
toolchains: paths.toolchains,
|
|
4338
|
+
verifiedDistribution: options.verifiedDistribution
|
|
4339
|
+
});
|
|
4340
|
+
const runner = new ServerRunner({
|
|
4341
|
+
runtimeExecutable: paths.runtimeExecutable,
|
|
4342
|
+
toolchains: paths.toolchains,
|
|
4343
|
+
cacheDirectory: path.join(paths.cacheDirectory, "runtime"),
|
|
4344
|
+
runtimeDrivers: options.runtimeDrivers,
|
|
4345
|
+
additionalCostBaselines: options.additionalCostBaselines,
|
|
4346
|
+
verifiedDistribution: options.verifiedDistribution
|
|
4347
|
+
});
|
|
4348
|
+
return await createEngine({
|
|
4349
|
+
compiler,
|
|
4350
|
+
runner,
|
|
4351
|
+
artifactStore: options.artifactCache === false ? void 0 : new FileSystemArtifactStore(path.join(paths.cacheDirectory, "artifacts")),
|
|
4352
|
+
judge: options.judge,
|
|
4353
|
+
dependencyManager: createDefaultDependencyManager(new FileSystemDependencyCache(path.join(paths.cacheDirectory, "dependencies")))
|
|
4354
|
+
});
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
throw asWasmOjError(error, {
|
|
4357
|
+
code: "initialization-failure",
|
|
4358
|
+
stage: "initialize",
|
|
4359
|
+
retryable: false
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
async function verifyExecutable(file, label) {
|
|
4364
|
+
const metadata = await lstat(file);
|
|
4365
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`${label} must be a real regular file: '${file}'.`);
|
|
4366
|
+
await access(file, constants.X_OK);
|
|
4367
|
+
}
|
|
4368
|
+
async function verifyToolchainSources(toolchains) {
|
|
4369
|
+
for (const directory of serverToolchainDirectories(toolchains)) {
|
|
4370
|
+
const metadata = await lstat(directory);
|
|
4371
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Toolchain package path must be a real directory: '${directory}'.`);
|
|
4372
|
+
}
|
|
4373
|
+
for (const source of toolchains) for (const asset of source.descriptor.assets) {
|
|
4374
|
+
const file = serverToolchainAssetFile(toolchains, asset.path);
|
|
4375
|
+
const fileMetadata = await lstat(file);
|
|
4376
|
+
if (!fileMetadata.isFile() || fileMetadata.isSymbolicLink()) throw new Error(`Pinned toolchain asset must be a real regular file: '${file}'.`);
|
|
4377
|
+
if (fileMetadata.size !== asset.bytes) throw new Error(`Pinned toolchain asset '${file}' has ${fileMetadata.size} bytes; expected ${asset.bytes}.`);
|
|
4378
|
+
const actual = await digestFile(file);
|
|
4379
|
+
if (actual !== asset.sha256) throw new Error(`Pinned toolchain asset '${file}' has digest ${actual}; expected ${asset.sha256}.`);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
async function digestFile(file) {
|
|
4383
|
+
const digest = createHash("sha256");
|
|
4384
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4385
|
+
try {
|
|
4386
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) digest.update(chunk);
|
|
4387
|
+
return digest.digest("hex");
|
|
4388
|
+
} finally {
|
|
4389
|
+
await handle.close();
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
//#endregion
|
|
4393
|
+
export { FileSystemArtifactStore, FileSystemDependencyCache, ServerCompiler, ServerRunner, createServerEngine, createVerifiedServerDistribution, resolveServerPaths };
|