crossbind 2.0.0-beta.63 → 2.0.0-beta.64
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/AGENTS.md +2 -1
- package/package.json +2 -2
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildNode.js +4 -6
- package/src/actions/buildWasiCommand.js +10 -5
- package/src/actions/buildWasm.js +24 -3
- package/src/actions/createImportedBridges.js +7 -0
- package/src/actions/createInterface.js +3 -3
- package/src/actions/createLib.js +14 -2
- package/src/actions/getData.js +2 -1
- package/src/actions/getEnv.js +7 -0
- package/src/actions/runnerCommands.js +16 -4
- package/src/actions/writeRuntimeEntry.js +18 -7
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +51 -0
- package/src/assets/js-runtime/core.js +5 -4
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/bin.js +29 -179
- package/src/cli.js +14 -0
- package/src/integration/getCrossbindScript.js +4 -3
- package/src/program.js +169 -0
- package/src/state/index.js +4 -3
- package/src/state/loadConfig.js +4 -11
- package/src/utils/appRustCrates.js +4 -0
- package/src/utils/bridgeExtras.js +74 -6
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cppDts.js +2 -0
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/packageLicense.js +4 -2
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/rustBridgeGen.js +13 -4
- package/src/utils/rustMt.js +5 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/swigInterface.js +5 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/targets.js +11 -0
- package/src/utils/wasiHttp.js +50 -0
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## What lives here
|
|
6
6
|
|
|
7
|
-
- `src/
|
|
7
|
+
- `src/cli.js` — `crossbind` CLI entry: `login`, `logout`, `usage`, `account`, `config` and `--version` run from `src/program.js` without loading a project (only `config list -t all|project` loads it); every other command goes to `src/bin.js`.
|
|
8
|
+
- `src/bin.js` — the commands that need a project; commander commands, target filtering and top-level orchestration.
|
|
8
9
|
- `src/index.js` — public API barrel re-exported as the `crossbind` npm package (`createLib`, `buildWasm`, `state`, `getData`, `isSourceNewer`, …).
|
|
9
10
|
- `src/actions/` — the actual build steps. **Most edits land here.**
|
|
10
11
|
- `src/state/` — config loading + target matrix.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crossbind",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.64",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://crossbind.dev",
|
|
6
6
|
"repository": "https://github.com/crossbind/crossbind.git",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"node": ">=24"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"crossbind": "./src/
|
|
13
|
+
"crossbind": "./src/cli.js"
|
|
14
14
|
},
|
|
15
15
|
"main": "src/index.js",
|
|
16
16
|
"exports": {
|
|
Binary file
|
package/src/actions/buildJs.js
CHANGED
|
@@ -5,7 +5,7 @@ import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
|
5
5
|
import commonjs from '@rollup/plugin-commonjs';
|
|
6
6
|
import virtual from '@rollup/plugin-virtual';
|
|
7
7
|
import state from '../state/index.js';
|
|
8
|
-
import
|
|
8
|
+
import getEnv from './getEnv.js';
|
|
9
9
|
|
|
10
10
|
const SUPPRESSED_WARN_CODES = new Set([
|
|
11
11
|
'MISSING_GLOBAL_NAME',
|
|
@@ -53,14 +53,7 @@ options.edge = options.browser;
|
|
|
53
53
|
|
|
54
54
|
export default async function buildJS(target) {
|
|
55
55
|
const entryJS = `${state.config.paths.cli}/assets/js-runtime/${target.runtimeEnv}.js`;
|
|
56
|
-
const
|
|
57
|
-
const resolvedEnv = Object.fromEntries(
|
|
58
|
-
Object.entries(rawEnv).map(([key, value]) => [
|
|
59
|
-
key,
|
|
60
|
-
typeof value === 'function' ? value(state, target) : value,
|
|
61
|
-
]),
|
|
62
|
-
);
|
|
63
|
-
const env = JSON.stringify(resolvedEnv);
|
|
56
|
+
const env = JSON.stringify(getEnv(target));
|
|
64
57
|
const systemConfig = `export default {
|
|
65
58
|
env: ${env},
|
|
66
59
|
useWorker: !!globalThis.Worker,
|
package/src/actions/buildNode.js
CHANGED
|
@@ -8,6 +8,8 @@ import virtual from '@rollup/plugin-virtual';
|
|
|
8
8
|
import run from './run.js';
|
|
9
9
|
import getLinkInputs from './getLinkInputs.js';
|
|
10
10
|
import getData from './getData.js';
|
|
11
|
+
import getEnv from './getEnv.js';
|
|
12
|
+
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
11
13
|
import { suppressNodeBuiltinWarnings } from './buildJs.js';
|
|
12
14
|
import state from '../state/index.js';
|
|
13
15
|
import logger from '../utils/logger.js';
|
|
@@ -52,11 +54,6 @@ function linkInputs(target) {
|
|
|
52
54
|
return { libs, linkArgs, extraSources };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function envOf(target) {
|
|
56
|
-
return Object.fromEntries(Object.entries(getData('env', target))
|
|
57
|
-
.map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
57
|
// A package publishes its addons in <package>-<platform>-<arch> by listing them as optional
|
|
61
58
|
// dependencies, which is also how npm learns to install only the one for its machine.
|
|
62
59
|
function listsAddonPackage(target) {
|
|
@@ -82,7 +79,7 @@ export function nodeLoaderConfig(target) {
|
|
|
82
79
|
const addonPackage = addonPackagePattern(served);
|
|
83
80
|
const loaded = addonPackage ? served.filter(listsAddonPackage) : served;
|
|
84
81
|
return {
|
|
85
|
-
env: Object.fromEntries(loaded.map((t) => [`${t.platform}-${t.arch}`,
|
|
82
|
+
env: Object.fromEntries(loaded.map((t) => [`${t.platform}-${t.arch}`, getEnv(t)])),
|
|
86
83
|
general: { name: state.config.general.name },
|
|
87
84
|
paths: { addon: target.addonPattern, ...(addonPackage ? { addonPackage } : {}) },
|
|
88
85
|
};
|
|
@@ -162,6 +159,7 @@ export default async function buildNode(target, options = {}) {
|
|
|
162
159
|
|
|
163
160
|
const platformPrefix = `Node-${buildType}`;
|
|
164
161
|
logger.startStep(target, 'addon');
|
|
162
|
+
resetCMakeCacheForSource(`${build}/${platformPrefix}/${target.path}`, `${napiRoot}/cpp`, target);
|
|
165
163
|
run(null, [
|
|
166
164
|
'cmake', `${napiRoot}/cpp`,
|
|
167
165
|
`-DCMAKE_BUILD_TYPE=${buildType}`,
|
|
@@ -7,14 +7,18 @@ import logger from '../utils/logger.js';
|
|
|
7
7
|
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
8
8
|
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
9
9
|
import { wasiCxxFlags, WASI_LINK_LIBS } from '../utils/wasiToolchain.js';
|
|
10
|
+
import { stageWasiHttpLink, wasiHttpFiles } from '../utils/wasiHttp.js';
|
|
10
11
|
|
|
11
12
|
// Links a platform:'wasi' target into one command module: main() is the entry, deps are DCE'd, data lands as a real dir for --dir preopens.
|
|
13
|
+
// An http target links into a wasi:http/service component instead, whose entry is crossbind's handler export.
|
|
12
14
|
export default async function buildWasiCommand(target, options = {}) {
|
|
13
15
|
const isProd = target.buildType === 'release';
|
|
14
16
|
const buildType = isProd ? 'Release' : 'Debug';
|
|
17
|
+
const isHttp = target.runtimeEnv === 'http';
|
|
18
|
+
const step = isHttp ? 'wasi http' : 'wasi command';
|
|
15
19
|
|
|
16
20
|
if (state.config.export?.type === 'cargo') {
|
|
17
|
-
logger.info(`[${target.path}]
|
|
21
|
+
logger.info(`[${target.path}] ${step} skipped (cargo packages have no cargo wasi triple)`);
|
|
18
22
|
return false;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -52,17 +56,17 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
52
56
|
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
53
57
|
}),
|
|
54
58
|
data: getData('data', target),
|
|
55
|
-
runtime: getFilesFingerprint([stubs]),
|
|
59
|
+
runtime: getFilesFingerprint([stubs, ...(isHttp ? wasiHttpFiles(state.config.paths.cli) : [])]),
|
|
56
60
|
}));
|
|
57
61
|
const linkChanged = !fs.existsSync(linkFingerprintFile)
|
|
58
62
|
|| fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
|
|
59
63
|
|
|
60
64
|
if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
|
|
61
|
-
logger.cachedStep(target,
|
|
65
|
+
logger.cachedStep(target, step);
|
|
62
66
|
return false;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
logger.startStep(target,
|
|
69
|
+
logger.startStep(target, step);
|
|
66
70
|
const t0 = performance.now();
|
|
67
71
|
// stubs.c lives outside the docker mount (paths.cli); stage it next to the artifact.
|
|
68
72
|
const stagedStubs = `${state.config.paths.build}/crossbind-wasi-stubs.c`;
|
|
@@ -71,6 +75,7 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
71
75
|
run(null, [
|
|
72
76
|
'wasi-clang++',
|
|
73
77
|
stagedStubs,
|
|
78
|
+
...(isHttp ? stageWasiHttpLink(state.config.paths.cli, `${state.config.paths.build}/crossbind-wasi-http`) : []),
|
|
74
79
|
...wasiCxxFlags(),
|
|
75
80
|
...(isProd ? ['-O3'] : ['-O0']),
|
|
76
81
|
...wasiFlags,
|
|
@@ -79,7 +84,7 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
79
84
|
'-o', `${state.config.paths.build}/${target.wasmName}`,
|
|
80
85
|
], null, target);
|
|
81
86
|
const ms = Math.round(performance.now() - t0);
|
|
82
|
-
logger.doneStep(target,
|
|
87
|
+
logger.doneStep(target, step, ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
83
88
|
|
|
84
89
|
// Dep data lands as a real folder next to the artifact for --dir preopens.
|
|
85
90
|
Object.entries(getData('data', target)).forEach(([key, value]) => {
|
package/src/actions/buildWasm.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import run from './run.js';
|
|
5
5
|
import getLinkInputs from './getLinkInputs.js';
|
|
6
6
|
import getData from './getData.js';
|
|
7
|
+
import getEnv from './getEnv.js';
|
|
7
8
|
import buildJs from './buildJs.js';
|
|
8
9
|
import triggerExtensions from './extensions.js';
|
|
9
10
|
import state from '../state/index.js';
|
|
@@ -14,9 +15,10 @@ import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
|
14
15
|
import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgumentGuards.js';
|
|
15
16
|
import { separateCallArguments } from '../utils/embindCallArguments.js';
|
|
16
17
|
import { queueJspiCalls } from '../utils/embindJspiQueue.js';
|
|
18
|
+
import { releaseOwnedHandles } from '../utils/embindObjectLifetime.js';
|
|
17
19
|
import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
|
|
18
20
|
|
|
19
|
-
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/pthreadMainScript.js']
|
|
21
|
+
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/pthreadMainScript.js']
|
|
20
22
|
.map((file) => fileURLToPath(new URL(file, import.meta.url)));
|
|
21
23
|
|
|
22
24
|
// embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
|
|
@@ -64,6 +66,17 @@ function queueJspiEmbindCalls(target) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
// Objects JavaScript owns outside a smart pointer were freed only by .delete() (utils/embindObjectLifetime.js);
|
|
70
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
71
|
+
function releaseOwnedObjects(target) {
|
|
72
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
73
|
+
const { text, missed } = releaseOwnedHandles(fs.readFileSync(gluePath, 'utf8'));
|
|
74
|
+
fs.writeFileSync(gluePath, text);
|
|
75
|
+
if (missed) {
|
|
76
|
+
logger.error('embind object lifetime rewrite missed (emscripten glue format changed?): returned values and constructed vectors are freed only by .delete()');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
// A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
|
|
68
81
|
function pointPthreadSpawn(target) {
|
|
69
82
|
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
@@ -173,6 +186,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
173
186
|
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
174
187
|
}),
|
|
175
188
|
data: getData('data', target),
|
|
189
|
+
// The JS the link step writes carries the env.
|
|
190
|
+
env: getEnv(target),
|
|
176
191
|
runtime: getFilesFingerprint(runtimeAssets),
|
|
177
192
|
rustSources: getFilesFingerprint(rustSources),
|
|
178
193
|
}));
|
|
@@ -233,6 +248,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
233
248
|
guardArgumentConversions(target);
|
|
234
249
|
separateEmbindCalls(target);
|
|
235
250
|
queueJspiEmbindCalls(target);
|
|
251
|
+
releaseOwnedObjects(target);
|
|
236
252
|
await buildJs(target);
|
|
237
253
|
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
238
254
|
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
@@ -257,12 +273,13 @@ export default async function buildWasm(target, options = {}) {
|
|
|
257
273
|
// it any linked Rust package archive fails with undefined crossbind_* symbols.
|
|
258
274
|
...linkLibs, ...rustSources,
|
|
259
275
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
260
|
-
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
276
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
261
277
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
262
278
|
// See the GROWABLE_ARRAYBUFFERS note in the browser block (Firefox/WebKit TextDecoder).
|
|
263
279
|
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
280
|
+
'-s', 'FORCE_FILESYSTEM=1',
|
|
264
281
|
'-s', 'ENVIRONMENT=web',
|
|
265
|
-
'-s', 'EXPORTED_RUNTIME_METHODS=["ENV", "getExceptionMessage"]',
|
|
282
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
|
|
266
283
|
'-fwasm-exceptions',
|
|
267
284
|
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
268
285
|
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
@@ -275,6 +292,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
275
292
|
guardArgumentConversions(target);
|
|
276
293
|
separateEmbindCalls(target);
|
|
277
294
|
queueJspiEmbindCalls(target);
|
|
295
|
+
releaseOwnedObjects(target);
|
|
278
296
|
await buildJs(target);
|
|
279
297
|
logger.doneStep(target, 'js');
|
|
280
298
|
}
|
|
@@ -292,6 +310,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
292
310
|
// '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
293
311
|
'-s', 'FORCE_FILESYSTEM=1',
|
|
294
312
|
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
|
|
313
|
+
// node.cpp answers fsync and fdatasync for host files.
|
|
314
|
+
'-Wl,--wrap=__wasi_fd_sync', '-Wl,--wrap=__syscall_fdatasync',
|
|
295
315
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
296
316
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
|
|
297
317
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -311,6 +331,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
311
331
|
guardArgumentConversions(target);
|
|
312
332
|
separateEmbindCalls(target);
|
|
313
333
|
queueJspiEmbindCalls(target);
|
|
334
|
+
releaseOwnedObjects(target);
|
|
314
335
|
await buildJs(target);
|
|
315
336
|
if (emccFlags.includes('FETCH')) {
|
|
316
337
|
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
@@ -34,6 +34,13 @@ function resolveImport(specifier, importer, targets) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Whether a binary binds what the app's own sources import. A Node binary does: its module hooks resolve those
|
|
38
|
+
// imports. A browser binary only with --bind-imports: without a bundler a page cannot resolve a header import
|
|
39
|
+
// unless it maps the specifiers itself, as the playground's sandbox does. Nothing on an edge runtime resolves one.
|
|
40
|
+
export function bindsAppImports(target, { bindImports = false } = {}) {
|
|
41
|
+
return target.runtimeEnv === 'node' || (bindImports && target.runtimeEnv === 'browser');
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
// The native files the app's own sources import, by real path, each under the first target whose packages ship it
|
|
38
45
|
// and with the bare specifiers that name it, which Node cannot resolve on its own.
|
|
39
46
|
export function findAppNativeImports(targets) {
|
|
@@ -27,10 +27,10 @@ import run, { cxxPreprocessorFor } from './run.js';
|
|
|
27
27
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
28
28
|
|
|
29
29
|
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
30
|
-
const INTERFACE_FORMAT = 'swig-macros-
|
|
30
|
+
const INTERFACE_FORMAT = 'swig-macros-4';
|
|
31
31
|
// Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
|
|
32
32
|
// bridges from a fork before field bindings and constants are rebuilt.
|
|
33
|
-
const BRIDGE_FORMAT = 'swig-extras-
|
|
33
|
+
const BRIDGE_FORMAT = 'swig-extras-2';
|
|
34
34
|
const predefinedMacros = new Map();
|
|
35
35
|
const typeDefinitions = new Map();
|
|
36
36
|
|
|
@@ -188,7 +188,7 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
|
|
|
188
188
|
const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
|
|
189
189
|
|
|
190
190
|
const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
|
|
191
|
-
const extras = interfaceExtras(named, macros.table);
|
|
191
|
+
const extras = interfaceExtras(named, macros.table, fs.readFileSync(headerOrModuleFilePath, 'utf8'));
|
|
192
192
|
const content = buildInterfaceContent({
|
|
193
193
|
moduleName: fileName.toUpperCase(),
|
|
194
194
|
headerPath,
|
package/src/actions/createLib.js
CHANGED
|
@@ -10,11 +10,13 @@ import triggerExtensions from './extensions.js';
|
|
|
10
10
|
import state from '../state/index.js';
|
|
11
11
|
import logger from '../utils/logger.js';
|
|
12
12
|
import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
|
|
13
|
+
import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
|
|
13
14
|
import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
|
|
14
15
|
import { withDependencyBridges } from '../utils/dependencyBridges.js';
|
|
15
16
|
import { targetArchiveFlags } from '../utils/archiveFlags.js';
|
|
16
17
|
import { conanInputsOf } from '../utils/conanDependencies.js';
|
|
17
18
|
import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
19
|
+
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
18
20
|
|
|
19
21
|
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
20
22
|
const sharedPlatforms = ['android'];
|
|
@@ -56,7 +58,12 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
56
58
|
const conanInputs = conanInputsOf(state.config);
|
|
57
59
|
const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
|
|
58
60
|
const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
|
|
59
|
-
const
|
|
61
|
+
const keyInputs = conanInputs ? [flagsInputs, conanInputs] : flagsInputs;
|
|
62
|
+
// An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
|
|
63
|
+
const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
|
|
64
|
+
? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
|
|
65
|
+
: null;
|
|
66
|
+
const flagsFingerprint = getContentHash(JSON.stringify(httpHeader ? [keyInputs, httpHeader] : keyInputs));
|
|
60
67
|
const flagsChanged = !fs.existsSync(flagsFingerprintFile)
|
|
61
68
|
|| fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
|
|
62
69
|
|
|
@@ -142,7 +149,11 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
142
149
|
buildEnv.params.push('-e', e);
|
|
143
150
|
});
|
|
144
151
|
} else {
|
|
145
|
-
|
|
152
|
+
// The source of an http build includes <crossbind/http.h>.
|
|
153
|
+
const headerDirs = target.platform === 'wasi'
|
|
154
|
+
? [...(options.headerDirs ?? []), stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)]
|
|
155
|
+
: options.headerDirs;
|
|
156
|
+
buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
|
|
146
157
|
|
|
147
158
|
triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
|
|
148
159
|
|
|
@@ -229,6 +240,7 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
229
240
|
stageWasiStubs();
|
|
230
241
|
buildParams.push(`-DCMAKE_EXE_LINKER_FLAGS=${[...wasiCxxFlags(), wasiStubObj, ...WASI_LINK_LIBS].join(' ')}`);
|
|
231
242
|
}
|
|
243
|
+
resetCMakeCacheForSource(buildPath, cmakeDir, target);
|
|
232
244
|
run(null, [
|
|
233
245
|
target.platform === 'ios' && state.config.build?.useIOSCMake ? 'ios-cmake' : 'cmake', cmakeDir,
|
|
234
246
|
`-DCMAKE_BUILD_TYPE=${buildType}`,
|
package/src/actions/getData.js
CHANGED
|
@@ -30,7 +30,8 @@ function getRecursiveData(obj, config, dependency, field, target) {
|
|
|
30
30
|
obj[dKey] = [...(obj[dKey] || []), ...value];
|
|
31
31
|
} else if (typeof value === 'object') {
|
|
32
32
|
obj[dKey] = { ...(obj[dKey] || {}), ...value };
|
|
33
|
-
} else {
|
|
33
|
+
} else if (!Object.hasOwn(obj, dKey)) {
|
|
34
|
+
// The app comes before its dependencies and a package before its own, so the nearest value stays.
|
|
34
35
|
obj[dKey] = value;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import state from '../state/index.js';
|
|
2
|
+
import getData from './getData.js';
|
|
3
|
+
|
|
4
|
+
export default function getEnv(target) {
|
|
5
|
+
return Object.fromEntries(Object.entries(getData('env', target))
|
|
6
|
+
.map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
|
|
7
|
+
}
|
|
@@ -40,6 +40,7 @@ const BOOT_SCRIPT = [
|
|
|
40
40
|
const VERSION = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
41
41
|
|
|
42
42
|
const runnerName = (role) => `crossbind-runner-${role}`;
|
|
43
|
+
const localRunnerName = (role) => `crossbind-local-runner-${role}`;
|
|
43
44
|
const runnerFiles = () => fs.readdirSync(RUNNER_SOURCE).filter((file) => file.endsWith('.js'));
|
|
44
45
|
// Google ships the linux NDK for x86_64 only, so an android runner is always amd64.
|
|
45
46
|
const platformOf = (role) => (role === 'android' ? 'linux/amd64' : undefined);
|
|
@@ -62,7 +63,7 @@ function assertRole(role) {
|
|
|
62
63
|
export function runnerStartArgs({ role, port, host }) {
|
|
63
64
|
const platform = platformOf(role);
|
|
64
65
|
return [
|
|
65
|
-
'run', '-d', '--name',
|
|
66
|
+
'run', '-d', '--name', localRunnerName(role), '--restart', 'unless-stopped',
|
|
66
67
|
...(platform ? ['--platform', platform] : []),
|
|
67
68
|
...DOCKER_RUN_SECURITY_ARGS,
|
|
68
69
|
'-p', `${host}:${port}:${RUNNER_PORT}`,
|
|
@@ -89,13 +90,24 @@ export function startRunner({ role = 'web', port = runnerHostPort(role), host =
|
|
|
89
90
|
});
|
|
90
91
|
// A token taken from the environment is shown by its variable, so the line printed for it puts no secret in a log.
|
|
91
92
|
return {
|
|
92
|
-
name:
|
|
93
|
+
name: localRunnerName(role), url: `http://${host}:${port}`, token: secret, displayToken: fromEnvironment ? '$CROSSBIND_RUNNER_TOKEN' : secret,
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
// A container that is not there is already deleted: docker reports one on stderr even with -f, and some engines exit 1.
|
|
98
|
+
function removeContainer(name) {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync('docker', ['rm', '-f', name], { stdio: ['ignore', 'inherit', 'pipe'] });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!/no such container/i.test(String(error.stderr))) throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function deleteRunner({ role = 'web' } = {}) {
|
|
97
107
|
assertRole(role);
|
|
98
|
-
|
|
108
|
+
removeContainer(localRunnerName(role));
|
|
109
|
+
// An older crossbind started its local runner under the name the deployed runners take.
|
|
110
|
+
removeContainer(runnerName(role));
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
function dockerfile(role) {
|
|
@@ -36,13 +36,10 @@ export default function writeRuntimeEntry(entry, headers, runtime) {
|
|
|
36
36
|
|
|
37
37
|
const IMPORT_HOOKS = new URL('../assets/node-import-hooks.mjs', import.meta.url);
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Node runs the register file from its real path, so the files it names are relative to that.
|
|
44
|
-
const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
|
|
45
|
-
const natives = headers.map((header) => {
|
|
39
|
+
// Each bound header and Rust import by its real path relative to dir, with the bare specifiers that name it (imports,
|
|
40
|
+
// from findAppNativeImports) and its names, each with the module's name for it where the two differ.
|
|
41
|
+
function nativesOf(headers, imports, dir) {
|
|
42
|
+
return headers.map((header) => {
|
|
46
43
|
const file = upath.normalize(fs.realpathSync(header.file));
|
|
47
44
|
return {
|
|
48
45
|
file: upath.relative(dir, file),
|
|
@@ -50,6 +47,13 @@ export function writeImportHooks(entry, headers, imports, extensions) {
|
|
|
50
47
|
names: uniqueBindings(namesOf(header)).map(({ local, wire }) => (local === wire ? local : { local, wire })),
|
|
51
48
|
};
|
|
52
49
|
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The module `node --import` loads ahead of the app, so the app imports the bound headers themselves.
|
|
53
|
+
export function writeImportHooks(entry, headers, imports, extensions) {
|
|
54
|
+
// Node runs the register file from its real path, so the files it names are relative to that.
|
|
55
|
+
const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
|
|
56
|
+
const natives = nativesOf(headers, imports, dir);
|
|
53
57
|
writeIfChanged(`${dir}/hooks.mjs`, fs.readFileSync(IMPORT_HOOKS, 'utf8'));
|
|
54
58
|
writeIfChanged(entry.replace(/\.mjs$/, '.register.mjs'), [
|
|
55
59
|
'// Generated by crossbind - do not edit.',
|
|
@@ -60,6 +64,13 @@ export function writeImportHooks(entry, headers, imports, extensions) {
|
|
|
60
64
|
].join('\n'));
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
// Beside a browser binary built with --bind-imports: what a page that resolves the app's imports itself (the
|
|
68
|
+
// playground) maps them with, each file relative to the project.
|
|
69
|
+
export function writeImportManifest(file, headers, imports, projectDir) {
|
|
70
|
+
const natives = nativesOf(headers, imports, upath.normalize(fs.realpathSync(projectDir)));
|
|
71
|
+
writeIfChanged(file, `${JSON.stringify({ natives }, null, 4)}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
// Once the sources import nothing native, no start loads hooks an earlier build left; hooks.mjs stays while the other
|
|
64
75
|
// binary's hooks still import it.
|
|
65
76
|
export function removeImportHooks(entry) {
|
|
@@ -2,9 +2,26 @@
|
|
|
2
2
|
#include <emscripten/wasmfs.h>
|
|
3
3
|
#include <limits.h>
|
|
4
4
|
#include <unistd.h>
|
|
5
|
+
#include <wasi/api.h>
|
|
6
|
+
|
|
7
|
+
static backend_t hostBackend;
|
|
5
8
|
|
|
6
9
|
backend_t wasmfs_create_root_dir(void) {
|
|
7
|
-
|
|
10
|
+
hostBackend = wasmfs_create_node_backend("/");
|
|
11
|
+
return hostBackend;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Emscripten's Node backend traps on flush, which fsync and fdatasync call. A host file's writes have already
|
|
15
|
+
// reached the operating system, so both succeed there, as with NODERAWFS. buildWasm links these with --wrap.
|
|
16
|
+
extern "C" __wasi_errno_t __real___wasi_fd_sync(__wasi_fd_t fd);
|
|
17
|
+
extern "C" int __real___syscall_fdatasync(int fd);
|
|
18
|
+
|
|
19
|
+
extern "C" __wasi_errno_t __wrap___wasi_fd_sync(__wasi_fd_t fd) {
|
|
20
|
+
return wasmfs_get_backend_by_fd(fd) == hostBackend ? __WASI_ERRNO_SUCCESS : __real___wasi_fd_sync(fd);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
extern "C" int __wrap___syscall_fdatasync(int fd) {
|
|
24
|
+
return wasmfs_get_backend_by_fd(fd) == hostBackend ? 0 : __real___syscall_fdatasync(fd);
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
void wasmfs_before_preload(void) {
|
|
@@ -24,12 +24,44 @@ let nextEmbindId = 1;
|
|
|
24
24
|
const HANDLES = '__crossbindHandles';
|
|
25
25
|
let remoteModule = null;
|
|
26
26
|
|
|
27
|
+
// How many ids name each object, as a raw pointer handed out twice is one object.
|
|
28
|
+
const holders = new WeakMap();
|
|
29
|
+
|
|
27
30
|
function registerEmbindObject(obj) {
|
|
28
31
|
const id = nextEmbindId++;
|
|
29
32
|
embindRegistry.set(id, obj);
|
|
33
|
+
holders.set(obj, (holders.get(obj) ?? 0) + 1);
|
|
30
34
|
return id;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
// What JavaScript owns: an object behind a smart pointer, or a copy embind made of a returned value or an object
|
|
38
|
+
// JavaScript constructed, which utils/embindObjectLifetime.js marks. A raw pointer stays its library's.
|
|
39
|
+
const ownsObject = (obj) => {
|
|
40
|
+
const $$ = obj?.$$;
|
|
41
|
+
return Boolean($$ && ($$.smartPtr || $$.count?.owned));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const freeIfOwned = (obj) => {
|
|
45
|
+
if (ownsObject(obj) && !obj.isDeleted()) obj.delete();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// The worker side of a handle the main thread no longer reaches: its id goes, and the object with the last id.
|
|
49
|
+
const RELEASE = '__crossbindRelease';
|
|
50
|
+
|
|
51
|
+
function releaseEmbindObjects(ids) {
|
|
52
|
+
for (const id of ids) {
|
|
53
|
+
const obj = embindRegistry.get(id);
|
|
54
|
+
if (!embindRegistry.delete(id)) continue;
|
|
55
|
+
const left = holders.get(obj) - 1;
|
|
56
|
+
if (left > 0) {
|
|
57
|
+
holders.set(obj, left);
|
|
58
|
+
} else {
|
|
59
|
+
holders.delete(obj);
|
|
60
|
+
freeIfOwned(obj);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
// Comlink answers every member with a remote one: JSON.stringify and String() called toJSON and Symbol.toPrimitive
|
|
34
66
|
// on the worker's object, which has neither, and a member below the module is thenable. A handle answers them as a
|
|
35
67
|
// direct-mode object does.
|
|
@@ -39,11 +71,27 @@ const LOCAL_MEMBERS = {
|
|
|
39
71
|
[Symbol.toPrimitive]: (hint) => (hint === 'number' ? NaN : '[object Object]'),
|
|
40
72
|
};
|
|
41
73
|
|
|
74
|
+
// The main thread side: once a handle is collected, the worker lets go of its object, the ids collected in one task
|
|
75
|
+
// in one message.
|
|
76
|
+
const collected = [];
|
|
77
|
+
|
|
78
|
+
function releaseLater(id) {
|
|
79
|
+
collected.push(id);
|
|
80
|
+
if (collected.length > 1) return;
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
const ids = collected.splice(0);
|
|
83
|
+
Promise.resolve(remoteModule?.[RELEASE](ids)).catch(() => {});
|
|
84
|
+
}, 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleWatcher = typeof FinalizationRegistry === 'function' ? new FinalizationRegistry(releaseLater) : null;
|
|
88
|
+
|
|
42
89
|
function handleFor(id) {
|
|
43
90
|
const handle = new Proxy(remoteModule[HANDLES][id], {
|
|
44
91
|
get: (target, prop) => (Object.hasOwn(LOCAL_MEMBERS, prop) ? LOCAL_MEMBERS[prop] : target[prop]),
|
|
45
92
|
});
|
|
46
93
|
embindProxyIds.set(handle, id);
|
|
94
|
+
handleWatcher?.register(handle, id);
|
|
47
95
|
return handle;
|
|
48
96
|
}
|
|
49
97
|
|
|
@@ -126,6 +174,8 @@ Comlink.transferHandlers.set('embindVector', {
|
|
|
126
174
|
? { __comlinkProxy: true, __embindId: registerEmbindObject(elem) }
|
|
127
175
|
: elem;
|
|
128
176
|
}
|
|
177
|
+
// The main thread gets the array alone, so nothing can reach a vector the call handed over any more.
|
|
178
|
+
freeIfOwned(unwrapCoercionProxy(obj));
|
|
129
179
|
return [elements, []];
|
|
130
180
|
},
|
|
131
181
|
deserialize(elements) {
|
|
@@ -246,6 +296,7 @@ export function moduleRoot(m) {
|
|
|
246
296
|
value: new Proxy({}, { get: (_, id) => embindRegistry.get(Number(id)) }),
|
|
247
297
|
configurable: true,
|
|
248
298
|
});
|
|
299
|
+
Object.defineProperty(m, RELEASE, { value: releaseEmbindObjects, configurable: true });
|
|
249
300
|
return wrapWithVectorCoercion(m);
|
|
250
301
|
}
|
|
251
302
|
|
|
@@ -74,12 +74,13 @@ function buildLocateFile(config, adapter) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// WASMFS's FS.writeFile writes at the end of a file that exists (emsdk 6.0.9 js_api.cpp), where the classic FS
|
|
77
|
-
// replaces it; opening the file for writing empties it first.
|
|
77
|
+
// replaces it; opening the file for writing empties it first, unless the flags ask to append.
|
|
78
78
|
function replaceOnWriteFile(FS) {
|
|
79
79
|
const { writeFile } = FS;
|
|
80
|
-
FS.writeFile = (path, data) => {
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
FS.writeFile = (path, data, opts) => {
|
|
81
|
+
const appends = typeof opts?.flags === 'string' && opts.flags.startsWith('a');
|
|
82
|
+
if (!appends) FS.close(FS.open(path, 'w', opts?.mode));
|
|
83
|
+
return writeFile(path, data, opts);
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|