space-data-module-sdk 0.2.5 → 0.2.7
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/README.md +64 -2
- package/package.json +8 -2
- package/schemas/PluginManifest.fbs +9 -0
- package/src/bundle/constants.js +4 -1
- package/src/bundle/wasm.js +30 -2
- package/src/compiler/compileModule.js +12 -141
- package/src/compiler/emception.d.ts +60 -0
- package/src/compiler/emception.js +191 -0
- package/src/compiler/emceptionNode.js +51 -34
- package/src/compiler/index.d.ts +24 -0
- package/src/compiler/index.js +5 -0
- package/src/compliance/pluginCompliance.js +306 -1
- package/src/deployment/index.d.ts +224 -0
- package/src/deployment/index.js +1552 -0
- package/src/generated/orbpro/manifest/protocol-spec.d.ts +35 -3
- package/src/generated/orbpro/manifest/protocol-spec.js +120 -6
- package/src/generated/orbpro/manifest/protocol-spec.ts +191 -1
- package/src/index.d.ts +149 -3
- package/src/index.js +4 -0
- package/src/manifest/index.js +7 -0
- package/src/manifest/normalize.js +75 -5
- package/src/manifest/typeRefs.js +143 -0
- package/src/runtime/constants.js +13 -0
- package/src/runtime/index.d.ts +15 -0
- package/src/runtime/index.js +2 -0
- package/src/testing/index.d.ts +84 -0
- package/src/testing/index.js +414 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ A module built with this SDK is a `.wasm` artifact with:
|
|
|
34
34
|
- an exported `_start` entry when the artifact supports WASI command mode
|
|
35
35
|
- optional `sds.bundle` custom-section payloads for:
|
|
36
36
|
- manifest bytes
|
|
37
|
+
- resolved deployment plans and input bindings
|
|
37
38
|
- deployment authorization
|
|
38
39
|
- detached signatures
|
|
39
40
|
- encrypted transport envelopes
|
|
@@ -81,6 +82,14 @@ outputs. The reference invoke examples live in
|
|
|
81
82
|
- `manifest.hybrid.json`
|
|
82
83
|
- `module.c`
|
|
83
84
|
|
|
85
|
+
Input and output ports can independently declare regular `flatbuffer` payloads
|
|
86
|
+
or `aligned-binary` layouts. Mixed contracts are valid. When a port advertises
|
|
87
|
+
an `aligned-binary` layout, it must also advertise a regular `flatbuffer`
|
|
88
|
+
fallback for the same schema in the same accepted type set. A module can accept
|
|
89
|
+
a regular `OMM.fbs` request and emit an aligned-binary `StateVector.fbs`
|
|
90
|
+
response, provided the output port also declares the regular `StateVector.fbs`
|
|
91
|
+
fallback and the aligned type ref carries the correct layout metadata.
|
|
92
|
+
|
|
84
93
|
## Runtime Portability
|
|
85
94
|
|
|
86
95
|
The module format is language-neutral. A host can load modules from this SDK
|
|
@@ -115,6 +124,25 @@ This repo currently includes:
|
|
|
115
124
|
- a reference Node host and sync `sdn_host` bridge for the first hostcall
|
|
116
125
|
surface
|
|
117
126
|
|
|
127
|
+
## Testing
|
|
128
|
+
|
|
129
|
+
This repo now exposes a manifest-driven harness generator from
|
|
130
|
+
`space-data-module-sdk/testing` and two complementary integration suites:
|
|
131
|
+
|
|
132
|
+
- `npm run test:runtime-matrix`
|
|
133
|
+
- cross-language runtime smoke across the same WASM in Node.js, Go, Python,
|
|
134
|
+
Rust, Java, C#, and Swift
|
|
135
|
+
- covers method calling, aligned-binary envelope metadata preservation,
|
|
136
|
+
stdin/stdout/stderr, args, env, preopened filesystem access, and basic WASI
|
|
137
|
+
clock/time smoke
|
|
138
|
+
- `npm run test:host-surfaces`
|
|
139
|
+
- authoritative Node-host coverage for HTTP, TCP, UDP, TLS, WebSocket, MQTT,
|
|
140
|
+
process execution, timers, filesystem, and the sync `sdn_host` ABI
|
|
141
|
+
|
|
142
|
+
The detailed edge cases and the current WASI-vs-host portability boundary are
|
|
143
|
+
documented in
|
|
144
|
+
[`docs/testing-harness.md`](./docs/testing-harness.md).
|
|
145
|
+
|
|
118
146
|
## Install
|
|
119
147
|
|
|
120
148
|
```bash
|
|
@@ -161,8 +189,36 @@ import { createDeploymentAuthorization } from "space-data-module-sdk/auth";
|
|
|
161
189
|
import { encryptJsonForRecipient } from "space-data-module-sdk/transport";
|
|
162
190
|
import { compileModuleFromSource } from "space-data-module-sdk/compiler";
|
|
163
191
|
import { createSingleFileBundle } from "space-data-module-sdk/bundle";
|
|
192
|
+
import { validateDeploymentPlan } from "space-data-module-sdk/deployment";
|
|
193
|
+
import { generateManifestHarnessPlan } from "space-data-module-sdk/testing";
|
|
164
194
|
```
|
|
165
195
|
|
|
196
|
+
## Protocol Installation
|
|
197
|
+
|
|
198
|
+
Modules can declare hosted protocol contracts in `manifest.protocols`.
|
|
199
|
+
|
|
200
|
+
Those declarations are for stable artifact identity:
|
|
201
|
+
|
|
202
|
+
- `wireId`
|
|
203
|
+
- `transportKind`
|
|
204
|
+
- `role`
|
|
205
|
+
- `specUri`
|
|
206
|
+
- hosting hints like `defaultPort` and `requireSecureTransport`
|
|
207
|
+
|
|
208
|
+
Concrete multiaddrs, peer IDs, and producer routing do not belong in the
|
|
209
|
+
canonical manifest. They belong in deployment metadata attached to the final
|
|
210
|
+
package or bundle.
|
|
211
|
+
|
|
212
|
+
This repo exposes that deployment surface from
|
|
213
|
+
`space-data-module-sdk/deployment`. Use it to:
|
|
214
|
+
|
|
215
|
+
- validate resolved protocol installations
|
|
216
|
+
- describe input bindings from producers to module ports
|
|
217
|
+
- attach a deployment plan to `sds.bundle`
|
|
218
|
+
|
|
219
|
+
The full contract split is documented in
|
|
220
|
+
[`docs/protocol-installation.md`](./docs/protocol-installation.md).
|
|
221
|
+
|
|
166
222
|
## Single-File Bundles
|
|
167
223
|
|
|
168
224
|
`sds.bundle` keeps module delivery to one file without changing WebAssembly
|
|
@@ -179,6 +235,9 @@ The reference path lives in
|
|
|
179
235
|
- the `go` and `python` directories show non-JS readers against the same
|
|
180
236
|
bundle contract
|
|
181
237
|
|
|
238
|
+
Standard bundle payloads now include the optional `deployment-plan` JSON entry
|
|
239
|
+
for resolved protocol installations and producer input bindings.
|
|
240
|
+
|
|
182
241
|
## Module Publication
|
|
183
242
|
|
|
184
243
|
Packages that publish SDN modules now use the canonical `sdn-module`
|
|
@@ -294,8 +353,11 @@ npm run check:compliance
|
|
|
294
353
|
```
|
|
295
354
|
|
|
296
355
|
Node.js `>=20` is required. The compiler uses `sdn-emception` and `flatc-wasm`
|
|
297
|
-
by default
|
|
298
|
-
|
|
356
|
+
by default.
|
|
357
|
+
|
|
358
|
+
If another repo needs the same compiler runtime, the package also exposes a
|
|
359
|
+
shared emception session at `space-data-module-sdk/compiler/emception` with
|
|
360
|
+
helpers for serialized command execution and virtual filesystem access.
|
|
299
361
|
|
|
300
362
|
## License
|
|
301
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-data-module-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/DigitalArsenal/space-data-module-sdk.git"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
|
-
"space-data-module": "
|
|
12
|
+
"space-data-module": "bin/space-data-module.js"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./src/index.js",
|
|
@@ -18,8 +18,12 @@
|
|
|
18
18
|
"./auth": "./src/auth/index.js",
|
|
19
19
|
"./transport": "./src/transport/index.js",
|
|
20
20
|
"./compiler": "./src/compiler/index.js",
|
|
21
|
+
"./compiler/emception": "./src/compiler/emception.js",
|
|
21
22
|
"./bundle": "./src/bundle/index.js",
|
|
23
|
+
"./deployment": "./src/deployment/index.js",
|
|
22
24
|
"./invoke": "./src/invoke/index.js",
|
|
25
|
+
"./runtime": "./src/runtime/index.js",
|
|
26
|
+
"./testing": "./src/testing/index.js",
|
|
23
27
|
"./standards": "./src/standards/index.js",
|
|
24
28
|
"./schemas/*": "./schemas/*"
|
|
25
29
|
},
|
|
@@ -30,6 +34,8 @@
|
|
|
30
34
|
],
|
|
31
35
|
"scripts": {
|
|
32
36
|
"test": "node --test",
|
|
37
|
+
"test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js",
|
|
38
|
+
"test:runtime-matrix": "SPACE_DATA_MODULE_SDK_ENABLE_RUNTIME_MATRIX=1 node --test test/runtime-matrix.test.js",
|
|
33
39
|
"start:lab": "node ./lab/server.js",
|
|
34
40
|
"check:compliance": "node ./bin/space-data-module.js check --repo-root .",
|
|
35
41
|
"generate:vectors": "node ./examples/single-file-bundle/generate-vectors.mjs",
|
|
@@ -134,6 +134,15 @@ table ProtocolSpec {
|
|
|
134
134
|
input_port_id: string;
|
|
135
135
|
output_port_id: string;
|
|
136
136
|
description: string;
|
|
137
|
+
wire_id: string;
|
|
138
|
+
transport_kind: string;
|
|
139
|
+
role: string;
|
|
140
|
+
spec_uri: string;
|
|
141
|
+
auto_install: bool = true;
|
|
142
|
+
advertise: bool = false;
|
|
143
|
+
discovery_key: string;
|
|
144
|
+
default_port: uint16;
|
|
145
|
+
require_secure_transport: bool = false;
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
/// Build artifact emitted by the plugin toolchain.
|
package/src/bundle/constants.js
CHANGED
|
@@ -5,4 +5,7 @@ export const DEFAULT_HASH_ALGORITHM = "sha256";
|
|
|
5
5
|
export const DEFAULT_MANIFEST_EXPORT_SYMBOL = "plugin_get_manifest_flatbuffer";
|
|
6
6
|
export const DEFAULT_MANIFEST_SIZE_SYMBOL =
|
|
7
7
|
"plugin_get_manifest_flatbuffer_size";
|
|
8
|
-
|
|
8
|
+
export const SDS_DEPLOYMENT_SECTION_NAME = "sds.deployment";
|
|
9
|
+
export const SDS_DEPLOYMENT_ENTRY_ID = "deployment-plan";
|
|
10
|
+
export const SDS_DEPLOYMENT_MEDIA_TYPE =
|
|
11
|
+
"application/vnd.space-data.module.deployment+json";
|
package/src/bundle/wasm.js
CHANGED
|
@@ -3,11 +3,14 @@ import { decodePluginManifest, encodePluginManifest } from "../manifest/index.js
|
|
|
3
3
|
import { toUint8Array } from "../runtime/bufferLike.js";
|
|
4
4
|
import { sha256Bytes } from "../utils/crypto.js";
|
|
5
5
|
import { bytesToHex } from "../utils/encoding.js";
|
|
6
|
+
import { createDeploymentPlanBundleEntry } from "../deployment/index.js";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_MANIFEST_EXPORT_SYMBOL,
|
|
8
9
|
DEFAULT_MANIFEST_SIZE_SYMBOL,
|
|
9
10
|
SDS_BUNDLE_SECTION_NAME,
|
|
10
11
|
SDS_CUSTOM_SECTION_PREFIX,
|
|
12
|
+
SDS_DEPLOYMENT_ENTRY_ID,
|
|
13
|
+
SDS_DEPLOYMENT_SECTION_NAME,
|
|
11
14
|
} from "./constants.js";
|
|
12
15
|
import {
|
|
13
16
|
decodeModuleBundle,
|
|
@@ -268,6 +271,9 @@ function normalizeStandardEntries(options = {}) {
|
|
|
268
271
|
description: "Transport envelope metadata.",
|
|
269
272
|
});
|
|
270
273
|
}
|
|
274
|
+
if (options.deploymentPlan !== undefined) {
|
|
275
|
+
entries.push(createDeploymentPlanBundleEntry(options.deploymentPlan));
|
|
276
|
+
}
|
|
271
277
|
return entries.filter((entry) => entry.payload !== null);
|
|
272
278
|
}
|
|
273
279
|
|
|
@@ -282,7 +288,12 @@ function normalizeAdditionalEntries(entries = []) {
|
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
async function withSha256(entry) {
|
|
285
|
-
const payloadBytes =
|
|
291
|
+
const payloadBytes =
|
|
292
|
+
toUint8Array(entry.payload) ??
|
|
293
|
+
((entry.payloadEncoding === "json-utf8" ||
|
|
294
|
+
moduleBundleEncodingToName(entry.payloadEncoding) === "json-utf8")
|
|
295
|
+
? canonicalBytes(entry.payload)
|
|
296
|
+
: normalizeBytes(entry.payload, `entry "${entry.entryId}" payload`));
|
|
286
297
|
return {
|
|
287
298
|
...entry,
|
|
288
299
|
payload: payloadBytes,
|
|
@@ -315,6 +326,17 @@ function buildParsedEntries(bundle) {
|
|
|
315
326
|
parsedEntry.decodedManifest = null;
|
|
316
327
|
}
|
|
317
328
|
}
|
|
329
|
+
if (
|
|
330
|
+
parsedEntry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
|
|
331
|
+
parsedEntry.sectionName === SDS_DEPLOYMENT_SECTION_NAME
|
|
332
|
+
) {
|
|
333
|
+
parsedEntry.decodedDeploymentPlan =
|
|
334
|
+
parsedEntry.payloadEncodingName === "json-utf8" &&
|
|
335
|
+
parsedEntry.decodedPayload &&
|
|
336
|
+
typeof parsedEntry.decodedPayload === "object"
|
|
337
|
+
? parsedEntry.decodedPayload
|
|
338
|
+
: null;
|
|
339
|
+
}
|
|
318
340
|
return parsedEntry;
|
|
319
341
|
});
|
|
320
342
|
}
|
|
@@ -432,16 +454,22 @@ export async function parseSingleFileBundle(bytes, options = {}) {
|
|
|
432
454
|
manifest = null;
|
|
433
455
|
}
|
|
434
456
|
}
|
|
457
|
+
const deploymentEntry =
|
|
458
|
+
parsedEntries.find(
|
|
459
|
+
(entry) =>
|
|
460
|
+
entry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
|
|
461
|
+
entry.sectionName === SDS_DEPLOYMENT_SECTION_NAME,
|
|
462
|
+
) ?? null;
|
|
435
463
|
return {
|
|
436
464
|
wasmBytes,
|
|
437
465
|
bundleBytes,
|
|
438
466
|
bundle,
|
|
439
467
|
entries: parsedEntries,
|
|
440
468
|
manifest,
|
|
469
|
+
deploymentPlan: deploymentEntry?.decodedDeploymentPlan ?? null,
|
|
441
470
|
customSections,
|
|
442
471
|
canonicalWasmBytes: canonical.canonicalWasmBytes,
|
|
443
472
|
canonicalModuleHash: canonical.hashBytes,
|
|
444
473
|
canonicalModuleHashHex: canonical.hashHex,
|
|
445
474
|
};
|
|
446
475
|
}
|
|
447
|
-
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
mkdir,
|
|
5
4
|
mkdtemp,
|
|
6
|
-
readFile,
|
|
7
5
|
rm,
|
|
8
6
|
writeFile,
|
|
9
7
|
} from "node:fs/promises";
|
|
10
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
11
|
-
import { promisify } from "node:util";
|
|
12
8
|
|
|
13
9
|
import {
|
|
14
10
|
createDeploymentAuthorization,
|
|
@@ -43,7 +39,6 @@ import {
|
|
|
43
39
|
import { sha256Bytes } from "../utils/crypto.js";
|
|
44
40
|
import { getWasmWallet } from "../utils/wasmCrypto.js";
|
|
45
41
|
|
|
46
|
-
const execFile = promisify(execFileCallback);
|
|
47
42
|
const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
43
|
|
|
49
44
|
function selectCompiler(language) {
|
|
@@ -89,14 +84,6 @@ async function getInvokeCppSupportFiles() {
|
|
|
89
84
|
return { runtimeHeaders, schemaHeaders };
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
async function writeFilesToDirectory(rootDir, files) {
|
|
93
|
-
for (const [relativePath, content] of Object.entries(files)) {
|
|
94
|
-
const filePath = path.join(rootDir, relativePath);
|
|
95
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
96
|
-
await writeFile(filePath, content, "utf8");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
87
|
async function writeFilesToEmception(emception, rootDir, files) {
|
|
101
88
|
for (const [relativePath, content] of Object.entries(files)) {
|
|
102
89
|
const filePath = path.posix.join(rootDir, relativePath);
|
|
@@ -235,101 +222,6 @@ async function compileWithEmception(options = {}) {
|
|
|
235
222
|
}
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
// System Emscripten — fallback to emcc/em++ on PATH
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
|
|
242
|
-
async function compileWithSystemToolchain(options = {}) {
|
|
243
|
-
const {
|
|
244
|
-
compilerCommand,
|
|
245
|
-
sourceCompilerCommand,
|
|
246
|
-
sourceExtension,
|
|
247
|
-
sourceCode,
|
|
248
|
-
manifestSource,
|
|
249
|
-
invokeHeaderSource,
|
|
250
|
-
invokeSource,
|
|
251
|
-
exportedSymbols,
|
|
252
|
-
outputPath,
|
|
253
|
-
compileOptions,
|
|
254
|
-
} = options;
|
|
255
|
-
const tempDir = await mkdtemp(
|
|
256
|
-
path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
|
|
257
|
-
);
|
|
258
|
-
const sourcePath = path.join(tempDir, `module.${sourceExtension}`);
|
|
259
|
-
const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.cpp");
|
|
260
|
-
const invokeHeaderPath = path.join(tempDir, "space_data_module_invoke.h");
|
|
261
|
-
const invokeSourcePath = path.join(tempDir, "plugin-invoke-bridge.cpp");
|
|
262
|
-
const sourceObjectPath = path.join(tempDir, "module.o");
|
|
263
|
-
const manifestObjectPath = path.join(tempDir, "plugin-manifest-exports.o");
|
|
264
|
-
const invokeObjectPath = path.join(tempDir, "plugin-invoke-bridge.o");
|
|
265
|
-
const resolvedOutputPath = path.resolve(
|
|
266
|
-
outputPath ?? path.join(tempDir, "module.wasm"),
|
|
267
|
-
);
|
|
268
|
-
const runtimeIncludeDir = path.join(tempDir, "flatbuffers-runtime");
|
|
269
|
-
|
|
270
|
-
const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
|
|
271
|
-
|
|
272
|
-
await writeFile(sourcePath, sourceCode, "utf8");
|
|
273
|
-
await writeFile(manifestSourcePath, manifestSource, "utf8");
|
|
274
|
-
await writeFile(invokeHeaderPath, invokeHeaderSource, "utf8");
|
|
275
|
-
await writeFile(invokeSourcePath, invokeSource, "utf8");
|
|
276
|
-
await writeFilesToDirectory(tempDir, schemaHeaders);
|
|
277
|
-
await writeFilesToDirectory(runtimeIncludeDir, runtimeHeaders);
|
|
278
|
-
|
|
279
|
-
const args = buildCompilerArgs(exportedSymbols, compileOptions);
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
await execFile(sourceCompilerCommand, [
|
|
283
|
-
"-c",
|
|
284
|
-
sourcePath,
|
|
285
|
-
`-I${tempDir}`,
|
|
286
|
-
"-o",
|
|
287
|
-
sourceObjectPath,
|
|
288
|
-
], { timeout: 120_000 });
|
|
289
|
-
|
|
290
|
-
await execFile(compilerCommand, [
|
|
291
|
-
"-c",
|
|
292
|
-
manifestSourcePath,
|
|
293
|
-
"-std=c++17",
|
|
294
|
-
`-I${tempDir}`,
|
|
295
|
-
`-I${runtimeIncludeDir}`,
|
|
296
|
-
"-o",
|
|
297
|
-
manifestObjectPath,
|
|
298
|
-
], { timeout: 120_000 });
|
|
299
|
-
|
|
300
|
-
await execFile(compilerCommand, [
|
|
301
|
-
"-c",
|
|
302
|
-
invokeSourcePath,
|
|
303
|
-
"-std=c++17",
|
|
304
|
-
`-I${tempDir}`,
|
|
305
|
-
`-I${runtimeIncludeDir}`,
|
|
306
|
-
"-o",
|
|
307
|
-
invokeObjectPath,
|
|
308
|
-
], { timeout: 120_000 });
|
|
309
|
-
|
|
310
|
-
await execFile(compilerCommand, [
|
|
311
|
-
sourceObjectPath,
|
|
312
|
-
manifestObjectPath,
|
|
313
|
-
invokeObjectPath,
|
|
314
|
-
...args,
|
|
315
|
-
"-o",
|
|
316
|
-
resolvedOutputPath,
|
|
317
|
-
], { timeout: 120_000 });
|
|
318
|
-
} catch (error) {
|
|
319
|
-
error.message =
|
|
320
|
-
`Compilation failed with ${compilerCommand}: ` +
|
|
321
|
-
(error.stderr || error.message);
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const wasmBytes = await readFile(resolvedOutputPath);
|
|
326
|
-
return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
// Public API
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
|
|
333
225
|
export async function compileModuleFromSource(options = {}) {
|
|
334
226
|
const manifest = options.manifest ?? {};
|
|
335
227
|
const sourceCode = String(options.sourceCode ?? "");
|
|
@@ -382,38 +274,17 @@ export async function compileModuleFromSource(options = {}) {
|
|
|
382
274
|
...options,
|
|
383
275
|
noEntry: includeCommandMain !== true,
|
|
384
276
|
};
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
outputPath: options.outputPath,
|
|
397
|
-
compileOptions,
|
|
398
|
-
});
|
|
399
|
-
} catch (error) {
|
|
400
|
-
if (error?.code !== "EMCEPTION_LOAD_FAILED") {
|
|
401
|
-
throw error;
|
|
402
|
-
}
|
|
403
|
-
result = await compileWithSystemToolchain({
|
|
404
|
-
compilerCommand: "em++",
|
|
405
|
-
sourceCompilerCommand: compiler.command,
|
|
406
|
-
sourceExtension: compiler.extension,
|
|
407
|
-
sourceCode,
|
|
408
|
-
manifestSource,
|
|
409
|
-
invokeHeaderSource,
|
|
410
|
-
invokeSource,
|
|
411
|
-
exportedSymbols,
|
|
412
|
-
outputPath: options.outputPath,
|
|
413
|
-
compileOptions,
|
|
414
|
-
});
|
|
415
|
-
compilerBackend = "em++ (system)";
|
|
416
|
-
}
|
|
277
|
+
const result = await compileWithEmception({
|
|
278
|
+
sourceCompilerCommand: compiler.command,
|
|
279
|
+
sourceExtension: compiler.extension,
|
|
280
|
+
sourceCode,
|
|
281
|
+
manifestSource,
|
|
282
|
+
invokeHeaderSource,
|
|
283
|
+
invokeSource,
|
|
284
|
+
exportedSymbols,
|
|
285
|
+
outputPath: options.outputPath,
|
|
286
|
+
compileOptions,
|
|
287
|
+
});
|
|
417
288
|
wasmBytes = result.wasmBytes;
|
|
418
289
|
resolvedOutputPath = result.outputPath;
|
|
419
290
|
tempDir = result.tempDir;
|
|
@@ -425,7 +296,7 @@ export async function compileModuleFromSource(options = {}) {
|
|
|
425
296
|
});
|
|
426
297
|
|
|
427
298
|
return {
|
|
428
|
-
compiler:
|
|
299
|
+
compiler: "em++ (emception)",
|
|
429
300
|
language: compiler.language,
|
|
430
301
|
outputPath: resolvedOutputPath,
|
|
431
302
|
tempDir,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface EmceptionCommandResult {
|
|
2
|
+
command: string;
|
|
3
|
+
exitCode: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type SharedEmceptionFileContent =
|
|
9
|
+
| string
|
|
10
|
+
| Uint8Array
|
|
11
|
+
| ArrayBuffer
|
|
12
|
+
| ArrayBufferView;
|
|
13
|
+
|
|
14
|
+
export interface SharedEmceptionHandle {
|
|
15
|
+
getRaw(): unknown;
|
|
16
|
+
exists(targetPath: string): boolean;
|
|
17
|
+
mkdirTree(directoryPath: string): void;
|
|
18
|
+
writeFile(filePath: string, content: SharedEmceptionFileContent): void;
|
|
19
|
+
writeFiles(
|
|
20
|
+
rootDir: string,
|
|
21
|
+
files: Record<string, SharedEmceptionFileContent>,
|
|
22
|
+
): void;
|
|
23
|
+
readFile(filePath: string): Uint8Array;
|
|
24
|
+
readFile(filePath: string, options: { encoding: "utf8" }): string;
|
|
25
|
+
removeTree(targetPath: string): void;
|
|
26
|
+
run(
|
|
27
|
+
command: string,
|
|
28
|
+
options?: { throwOnNonZero?: boolean },
|
|
29
|
+
): EmceptionCommandResult;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SharedEmceptionSession {
|
|
33
|
+
load(): Promise<unknown>;
|
|
34
|
+
withLock<T>(
|
|
35
|
+
task: (handle: SharedEmceptionHandle) => T | Promise<T>,
|
|
36
|
+
): Promise<T>;
|
|
37
|
+
exists(targetPath: string): Promise<boolean>;
|
|
38
|
+
mkdirTree(directoryPath: string): Promise<void>;
|
|
39
|
+
writeFile(
|
|
40
|
+
filePath: string,
|
|
41
|
+
content: SharedEmceptionFileContent,
|
|
42
|
+
): Promise<void>;
|
|
43
|
+
writeFiles(
|
|
44
|
+
rootDir: string,
|
|
45
|
+
files: Record<string, SharedEmceptionFileContent>,
|
|
46
|
+
): Promise<void>;
|
|
47
|
+
readFile(filePath: string): Promise<Uint8Array>;
|
|
48
|
+
readFile(filePath: string, options: { encoding: "utf8" }): Promise<string>;
|
|
49
|
+
removeTree(targetPath: string): Promise<void>;
|
|
50
|
+
run(
|
|
51
|
+
command: string,
|
|
52
|
+
options?: { throwOnNonZero?: boolean },
|
|
53
|
+
): Promise<EmceptionCommandResult>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createSharedEmceptionSession(): SharedEmceptionSession;
|
|
57
|
+
export function loadSharedEmception(): Promise<unknown>;
|
|
58
|
+
export function withSharedEmception<T>(
|
|
59
|
+
task: (handle: SharedEmceptionHandle) => T | Promise<T>,
|
|
60
|
+
): Promise<T>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getSharedEmceptionController,
|
|
5
|
+
loadEmception,
|
|
6
|
+
runWithEmceptionLock,
|
|
7
|
+
} from "./emceptionNode.js";
|
|
8
|
+
|
|
9
|
+
const TEXT_DECODER = new TextDecoder();
|
|
10
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
function normalizeFileContent(content) {
|
|
13
|
+
if (typeof content === "string") {
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
if (content instanceof Uint8Array) {
|
|
17
|
+
return content;
|
|
18
|
+
}
|
|
19
|
+
if (content instanceof ArrayBuffer) {
|
|
20
|
+
return new Uint8Array(content);
|
|
21
|
+
}
|
|
22
|
+
if (ArrayBuffer.isView(content)) {
|
|
23
|
+
return new Uint8Array(
|
|
24
|
+
content.buffer,
|
|
25
|
+
content.byteOffset,
|
|
26
|
+
content.byteLength,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
throw new TypeError(
|
|
30
|
+
"Emception file content must be a string, Uint8Array, ArrayBuffer, or ArrayBufferView.",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cloneReadBytes(value) {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return TEXT_ENCODER.encode(value);
|
|
37
|
+
}
|
|
38
|
+
const bytes = normalizeFileContent(value);
|
|
39
|
+
return new Uint8Array(bytes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function removeTree(emception, targetPath) {
|
|
43
|
+
const analysis = emception.FS.analyzePath(targetPath);
|
|
44
|
+
if (!analysis.exists) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const stat = emception.FS.stat(targetPath);
|
|
48
|
+
if (!emception.FS.isDir(stat.mode)) {
|
|
49
|
+
emception.FS.unlink(targetPath);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const entries = emception.FS.readdir(targetPath).filter(
|
|
53
|
+
(entry) => entry !== "." && entry !== "..",
|
|
54
|
+
);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
removeTree(emception, path.posix.join(targetPath, entry));
|
|
57
|
+
}
|
|
58
|
+
emception.FS.rmdir(targetPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeRunResult(command, result) {
|
|
62
|
+
const normalized = {
|
|
63
|
+
command,
|
|
64
|
+
exitCode: Number(result?.returncode ?? 0) >>> 0,
|
|
65
|
+
stdout: String(result?.stdout ?? ""),
|
|
66
|
+
stderr: String(result?.stderr ?? ""),
|
|
67
|
+
};
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function maybeThrowRunFailure(result, options = {}) {
|
|
72
|
+
if (options.throwOnNonZero === false || result.exitCode === 0) {
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
const detail = result.stderr || result.stdout || "unknown emception failure";
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Emception command failed with exit code ${result.exitCode}: ${result.command}\n${detail}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class SharedEmceptionHandle {
|
|
82
|
+
constructor(emception) {
|
|
83
|
+
this.emception = emception;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getRaw() {
|
|
87
|
+
return this.emception;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exists(targetPath) {
|
|
91
|
+
return this.emception.FS.analyzePath(targetPath).exists;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mkdirTree(directoryPath) {
|
|
95
|
+
this.emception.FS.mkdirTree(directoryPath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
writeFile(filePath, content) {
|
|
99
|
+
this.emception.FS.mkdirTree(path.posix.dirname(filePath));
|
|
100
|
+
this.emception.writeFile(filePath, normalizeFileContent(content));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
writeFiles(rootDir, files) {
|
|
104
|
+
for (const [relativePath, content] of Object.entries(files ?? {})) {
|
|
105
|
+
this.writeFile(path.posix.join(rootDir, relativePath), content);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
readFile(filePath, options = {}) {
|
|
110
|
+
const bytes = cloneReadBytes(this.emception.readFile(filePath));
|
|
111
|
+
if (options.encoding === "utf8") {
|
|
112
|
+
return TEXT_DECODER.decode(bytes);
|
|
113
|
+
}
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removeTree(targetPath) {
|
|
118
|
+
removeTree(this.emception, targetPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
run(command, options = {}) {
|
|
122
|
+
const result = normalizeRunResult(command, this.emception.run(command));
|
|
123
|
+
return maybeThrowRunFailure(result, options);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class SharedEmceptionSession {
|
|
128
|
+
constructor(controller = getSharedEmceptionController()) {
|
|
129
|
+
this.controller = controller;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async load() {
|
|
133
|
+
return this.controller.load();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async withLock(task) {
|
|
137
|
+
return this.controller.withLock(
|
|
138
|
+
(emception) => task(new SharedEmceptionHandle(emception)),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async exists(targetPath) {
|
|
143
|
+
return this.withLock((handle) => handle.exists(targetPath));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async mkdirTree(directoryPath) {
|
|
147
|
+
await this.withLock((handle) => {
|
|
148
|
+
handle.mkdirTree(directoryPath);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async writeFile(filePath, content) {
|
|
153
|
+
await this.withLock((handle) => {
|
|
154
|
+
handle.writeFile(filePath, content);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async writeFiles(rootDir, files) {
|
|
159
|
+
await this.withLock((handle) => {
|
|
160
|
+
handle.writeFiles(rootDir, files);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async readFile(filePath, options = {}) {
|
|
165
|
+
return this.withLock((handle) => handle.readFile(filePath, options));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async removeTree(targetPath) {
|
|
169
|
+
await this.withLock((handle) => {
|
|
170
|
+
handle.removeTree(targetPath);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async run(command, options = {}) {
|
|
175
|
+
return this.withLock((handle) => handle.run(command, options));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createSharedEmceptionSession() {
|
|
180
|
+
return new SharedEmceptionSession();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function loadSharedEmception() {
|
|
184
|
+
return loadEmception();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function withSharedEmception(task) {
|
|
188
|
+
return runWithEmceptionLock(
|
|
189
|
+
(emception) => task(new SharedEmceptionHandle(emception)),
|
|
190
|
+
);
|
|
191
|
+
}
|