crossbind 2.0.0-beta.60 → 2.0.0-beta.63
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 +8 -6
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +37 -9
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +27 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +190 -0
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +92 -95
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +318 -65
- package/src/actions/createLib.js +60 -43
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +70 -0
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +175 -17
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +124 -32
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +27 -13
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +290 -37
- package/src/index.js +6 -1
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +53 -19
- package/src/integration/getDependFilePath.js +18 -12
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +31 -20
- package/src/state/loadConfig.js +36 -24
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +17 -1
- package/src/utils/appSources.js +112 -0
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +23 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +134 -55
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +84 -0
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +56 -0
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/inputStamp.js +23 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linkLayout.js +8 -1
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +101 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +10 -5
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +51 -53
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +903 -253
- package/src/utils/rustMt.js +4 -0
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +86 -6
- package/src/utils/systemKeys.js +43 -3
- package/src/utils/targets.js +104 -3
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
- package/src/utils/cppFieldBindings.js +0 -23
package/src/actions/buildWasm.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import replace from 'replace';
|
|
4
4
|
import run from './run.js';
|
|
5
|
-
import
|
|
5
|
+
import getLinkInputs from './getLinkInputs.js';
|
|
6
6
|
import getData from './getData.js';
|
|
7
7
|
import buildJs from './buildJs.js';
|
|
8
8
|
import triggerExtensions from './extensions.js';
|
|
@@ -11,87 +11,84 @@ import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
|
|
|
11
11
|
import logger from '../utils/logger.js';
|
|
12
12
|
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
13
13
|
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
14
|
-
import
|
|
14
|
+
import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgumentGuards.js';
|
|
15
|
+
import { separateCallArguments } from '../utils/embindCallArguments.js';
|
|
16
|
+
import { queueJspiCalls } from '../utils/embindJspiQueue.js';
|
|
17
|
+
import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
|
|
18
|
+
|
|
19
|
+
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/pthreadMainScript.js']
|
|
20
|
+
.map((file) => fileURLToPath(new URL(file, import.meta.url)));
|
|
15
21
|
|
|
16
22
|
// embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
|
|
17
23
|
// A 64-bit parameter takes a BigInt or a Number that is a safe integer instead; the jsi adapter
|
|
18
24
|
// enforces the same rule natively. Each runtimeEnv links its own glue, so each one calls this.
|
|
19
|
-
function
|
|
25
|
+
function guardBigIntConversions(target) {
|
|
20
26
|
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
paths: [gluePath],
|
|
25
|
-
recursive: false,
|
|
26
|
-
silent: true,
|
|
27
|
-
});
|
|
28
|
-
const glue = fs.readFileSync(gluePath, 'utf8');
|
|
29
|
-
if (glue.includes('__embind_register_bigint') && !glue.includes('Number.isSafeInteger(value)')) {
|
|
27
|
+
const { text, missed } = guardBigIntArguments(fs.readFileSync(gluePath, 'utf8'));
|
|
28
|
+
fs.writeFileSync(gluePath, text);
|
|
29
|
+
if (missed) {
|
|
30
30
|
logger.error('bigint safe-integer rewrite missed (emscripten glue format changed?): 64-bit Number arguments round silently');
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Wrong-typed integer and enum arguments cross as 0 in a release build (utils/embindArgumentGuards.js);
|
|
35
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
36
|
+
function guardArgumentConversions(target) {
|
|
37
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
38
|
+
const { text, missed } = guardEmbindArguments(fs.readFileSync(gluePath, 'utf8'));
|
|
39
|
+
fs.writeFileSync(gluePath, text);
|
|
40
|
+
if (missed.length) {
|
|
41
|
+
logger.error(`embind argument rewrite missed ${missed.join(', ')} (emscripten glue format changed?): wrong-typed arguments cross as 0`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Overlapping calls of one embind function free each other's arguments (utils/embindCallArguments.js);
|
|
46
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
47
|
+
function separateEmbindCalls(target) {
|
|
48
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
49
|
+
const { text, missed } = separateCallArguments(fs.readFileSync(gluePath, 'utf8'));
|
|
50
|
+
fs.writeFileSync(gluePath, text);
|
|
51
|
+
if (missed) {
|
|
52
|
+
logger.error('embind per-call argument rewrite missed (emscripten glue format changed?): overlapping calls of one function free each other\'s arguments');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Suspended _JSPI calls that resume out of order overwrite each other's C stack (utils/embindJspiQueue.js);
|
|
57
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
58
|
+
function queueJspiEmbindCalls(target) {
|
|
59
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
60
|
+
const { text, missed } = queueJspiCalls(fs.readFileSync(gluePath, 'utf8'));
|
|
61
|
+
fs.writeFileSync(gluePath, text);
|
|
62
|
+
if (missed) {
|
|
63
|
+
logger.error('embind _JSPI queue rewrite missed (emscripten glue format changed?): suspended _JSPI calls can overwrite each other\'s C stack');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
|
|
68
|
+
function pointPthreadSpawn(target) {
|
|
69
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
70
|
+
const { text, missed } = spawnPthreadsFromMainScript(fs.readFileSync(gluePath, 'utf8'));
|
|
71
|
+
fs.writeFileSync(gluePath, text);
|
|
72
|
+
if (missed) {
|
|
73
|
+
logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
34
77
|
export default async function buildWasm(target, options = {}) {
|
|
35
78
|
const isProd = target.buildType === 'release';
|
|
36
|
-
const buildType = isProd ? 'Release' : 'Debug';
|
|
37
79
|
|
|
38
|
-
// Caller can opt out of the final emcc link entirely (e.g. when the
|
|
39
|
-
// package is consumed only as a static library by downstream builds).
|
|
40
|
-
if (state.config.export.bundle === false) {
|
|
41
|
-
logger.info(`[${target.path}] wasm+js skipped (export.bundle = false)`);
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
80
|
// Cargo packages only stage their staticlib; the consuming app links and bundles it.
|
|
45
81
|
if (state.config.export.type === 'cargo') {
|
|
46
82
|
logger.info(`[${target.path}] wasm+js skipped (cargo package - staticlib only)`);
|
|
47
83
|
return false;
|
|
48
84
|
}
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
|
|
57
|
-
// single fully-loaded Rust archive of the link (see the libstd rule below).
|
|
58
|
-
const appRustLibs = buildAppRustCrates(target, state.config.paths.cache, state.config.cargoDependencies ?? {});
|
|
59
|
-
const libs = [
|
|
60
|
-
...getDependLibs(target),
|
|
61
|
-
...appRustLibs,
|
|
62
|
-
sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
|
|
63
|
-
`${state.config.paths.build}/Bridge-${buildType}/${target.path}/lib${state.config.general.name}.a`,
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
// By default only the Bridge archive is --whole-archive'd (see
|
|
67
|
-
// linkLayout.js) so wasm-ld dead-code-eliminates everything unreferenced.
|
|
68
|
-
// Two escape hatches, both `export.wholeArchive: true`: on the APP it
|
|
69
|
-
// restores the legacy layout (every archive wholesale); on a LIBRARY's
|
|
70
|
-
// own config it keeps that library's archives whole in every consumer
|
|
71
|
-
// link (for members that self-register from static initializers).
|
|
72
|
-
const wholeArchiveAll = state.config.export.wholeArchive === true;
|
|
73
|
-
const wholeArchiveNames = new Set();
|
|
74
|
-
// Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
|
|
75
|
-
// may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
|
|
76
|
-
// with their keep symbol pinned (-u pulls just the registration object); manual-bindings
|
|
77
|
-
// crates have no keep symbol and fall back to whole-archive - safe only while they are the
|
|
78
|
-
// single loaded Rust archive.
|
|
79
|
-
const rustKeepFlags = [];
|
|
80
|
-
if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
|
|
81
|
-
state.config.dependencyParameters.getCmakeDepends(target).forEach((dep) => {
|
|
82
|
-
if (dep.export.wholeArchive === true) {
|
|
83
|
-
(dep.export.libName || []).forEach((name) => wholeArchiveNames.add(name));
|
|
84
|
-
}
|
|
85
|
-
if (dep.export.type === 'cargo') {
|
|
86
|
-
const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
|
|
87
|
-
const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
|
|
88
|
-
(dep.export.libName || []).forEach((name) => {
|
|
89
|
-
// wasm-ld's -u does NOT pull archive members (it just leaves an import);
|
|
90
|
-
// --export forces the symbol to resolve, dragging the registration object in.
|
|
91
|
-
if (isManual) wholeArchiveNames.add(name);
|
|
92
|
-
else rustKeepFlags.push(`-Wl,--export=crossbind_keep_${name}`);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
86
|
+
const {
|
|
87
|
+
libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
|
|
88
|
+
} = getLinkInputs(target, {
|
|
89
|
+
// wasm-ld's -u does NOT pull archive members (it just leaves an import);
|
|
90
|
+
// --export forces the symbol to resolve, dragging the registration object in.
|
|
91
|
+
keepFlag: (name) => `-Wl,--export=crossbind_keep_${name}`,
|
|
95
92
|
});
|
|
96
93
|
// The whole-archived app super-staticlib and a lazily-pulled package bridge object each
|
|
97
94
|
// carry rustc's allocator/panic shims (codegen-units=1 places them in the same object as
|
|
@@ -104,13 +101,19 @@ export default async function buildWasm(target, options = {}) {
|
|
|
104
101
|
|
|
105
102
|
// Any Rust archive in the link needs the web adapter TU: it provides the flat
|
|
106
103
|
// crossbind_embind_* C-ABI (typeid getters + passthroughs to emscripten's embind).
|
|
107
|
-
const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
|
|
108
|
-
|| state.config.dependencyParameters.getCmakeDepends(target).some((dep) => dep.export.type === 'cargo');
|
|
109
104
|
// The adapter ships in @crossbind/core-embind-rust (declared by the consumer, resolved here).
|
|
110
105
|
const rustSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/web.cpp`] : [];
|
|
111
106
|
|
|
112
107
|
const binary = getData('binary', target);
|
|
113
108
|
const emccFlags = [...(binary?.emccFlags || []), ...rustKeepFlags];
|
|
109
|
+
// Rust code expects a native-sized stack (rustc's own wasm targets reserve 1 MiB) where
|
|
110
|
+
// emscripten reserves 64 KiB, and a wasm stack has no guard page: an overflow overwrites static
|
|
111
|
+
// data without a trap. A size set in the config stays; a debug build also checks for overflow.
|
|
112
|
+
if (hasRust) {
|
|
113
|
+
const set = emccFlags.join(' ');
|
|
114
|
+
if (!/-s\s*(?:STACK_SIZE|TOTAL_STACK)=/.test(set)) emccFlags.push('-sSTACK_SIZE=1MB');
|
|
115
|
+
if (!isProd && !/-s\s*STACK_OVERFLOW_CHECK=/.test(set)) emccFlags.push('-sSTACK_OVERFLOW_CHECK=1');
|
|
116
|
+
}
|
|
114
117
|
|
|
115
118
|
triggerExtensions('buildWasm', 'beforeBuild', [emccFlags]);
|
|
116
119
|
|
|
@@ -160,7 +163,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
160
163
|
// The emcc arg tail is hardcoded per environment in this file, invisible to the
|
|
161
164
|
// emccFlags entry below - hashing the builder itself makes any inline-arg edit
|
|
162
165
|
// (a new -s flag, an EXPORTED_RUNTIME_METHODS change) a guaranteed cache miss.
|
|
163
|
-
builder
|
|
166
|
+
// The glue rewrites applied after the link count as the builder too.
|
|
167
|
+
builder: getFilesFingerprint([fileURLToPath(import.meta.url), ...GLUE_REWRITES]),
|
|
164
168
|
wholeArchiveAll,
|
|
165
169
|
wholeArchiveNames: [...wholeArchiveNames].sort(),
|
|
166
170
|
emccFlags,
|
|
@@ -189,12 +193,13 @@ export default async function buildWasm(target, options = {}) {
|
|
|
189
193
|
const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
|
|
190
194
|
run('em++', [
|
|
191
195
|
'-lembind',
|
|
196
|
+
// The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
|
|
197
|
+
...(isProd ? ['-O3'] : []),
|
|
192
198
|
...emccFlags,
|
|
193
199
|
// '-lwebsocket.js', '-sPROXY_POSIX_SOCKETS', '-sWEBSOCKET_DEBUG=1', '-sJSPI', '-g', '-sWASMFS',
|
|
194
200
|
'-s', 'FORCE_FILESYSTEM=1',
|
|
195
201
|
'-sEXPORT_NAME=Module2', // '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
196
202
|
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/browser.cpp`,
|
|
197
|
-
...(isProd ? ['-O3'] : []),
|
|
198
203
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
199
204
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
|
|
200
205
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -216,27 +221,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
216
221
|
const t1 = performance.now();
|
|
217
222
|
logger.doneStep(target, 'wasm');
|
|
218
223
|
logger.startStep(target, 'js');
|
|
219
|
-
|
|
220
|
-
// document.currentScript at LOAD time - undefined when the bridge is
|
|
221
|
-
// loaded as a module script, so workers would fetch "/undefined" and
|
|
222
|
-
// direct-mode mt init hangs. Inject Module.crossbindMainScript (set by the
|
|
223
|
-
// browser adapter from paths.worker/paths.js; a crossbind-specific key, as
|
|
224
|
-
// emscripten's own mainScriptUrlOrBlob trips emsdk 6 debug assertions
|
|
225
|
-
// on st targets) at SPAWN time instead. The previous spaced
|
|
226
|
-
// 'var _scriptName = ' rewrite no longer matched the minified glue and
|
|
227
|
-
// silently no-opped; the guard below turns any future format drift
|
|
228
|
-
// into a visible error instead.
|
|
229
|
-
replace({
|
|
230
|
-
regex: 'pthreadMainJs=_scriptName',
|
|
231
|
-
replacement: 'pthreadMainJs=Module["crossbindMainScript"]||_scriptName',
|
|
232
|
-
paths: [`${state.config.paths.build}/${target.rawJsName}`],
|
|
233
|
-
recursive: false,
|
|
234
|
-
silent: true,
|
|
235
|
-
});
|
|
236
|
-
const glue = fs.readFileSync(`${state.config.paths.build}/${target.rawJsName}`, 'utf8');
|
|
237
|
-
if (glue.includes('pthreadMainJs') && !glue.includes('Module["crossbindMainScript"]||_scriptName')) {
|
|
238
|
-
logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
|
|
239
|
-
}
|
|
224
|
+
pointPthreadSpawn(target);
|
|
240
225
|
/* replace({
|
|
241
226
|
regex: 'val === 10',
|
|
242
227
|
replacement: 'false',
|
|
@@ -244,7 +229,10 @@ export default async function buildWasm(target, options = {}) {
|
|
|
244
229
|
recursive: false,
|
|
245
230
|
silent: true,
|
|
246
231
|
}); */
|
|
247
|
-
|
|
232
|
+
guardBigIntConversions(target);
|
|
233
|
+
guardArgumentConversions(target);
|
|
234
|
+
separateEmbindCalls(target);
|
|
235
|
+
queueJspiEmbindCalls(target);
|
|
248
236
|
await buildJs(target);
|
|
249
237
|
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
250
238
|
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
@@ -261,12 +249,13 @@ export default async function buildWasm(target, options = {}) {
|
|
|
261
249
|
const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
|
|
262
250
|
run('em++', [
|
|
263
251
|
'-lembind',
|
|
252
|
+
// The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
|
|
253
|
+
...(isProd ? ['-O3'] : []),
|
|
264
254
|
...emccFlags,
|
|
265
255
|
'-sEXPORT_NAME=Module2',
|
|
266
256
|
// rustSources carries the embind-rust adapter TU (emval/json/tid hooks); without
|
|
267
257
|
// it any linked Rust package archive fails with undefined crossbind_* symbols.
|
|
268
258
|
...linkLibs, ...rustSources,
|
|
269
|
-
...(isProd ? ['-O3'] : []),
|
|
270
259
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
271
260
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
|
|
272
261
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -282,7 +271,10 @@ export default async function buildWasm(target, options = {}) {
|
|
|
282
271
|
const t1 = performance.now();
|
|
283
272
|
logger.doneStep(target, 'wasm');
|
|
284
273
|
logger.startStep(target, 'js');
|
|
285
|
-
|
|
274
|
+
guardBigIntConversions(target);
|
|
275
|
+
guardArgumentConversions(target);
|
|
276
|
+
separateEmbindCalls(target);
|
|
277
|
+
queueJspiEmbindCalls(target);
|
|
286
278
|
await buildJs(target);
|
|
287
279
|
logger.doneStep(target, 'js');
|
|
288
280
|
}
|
|
@@ -294,11 +286,12 @@ export default async function buildWasm(target, options = {}) {
|
|
|
294
286
|
|
|
295
287
|
run('em++', [
|
|
296
288
|
'-lembind',
|
|
289
|
+
// The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
|
|
290
|
+
...(isProd ? ['-O3'] : []),
|
|
297
291
|
...emccFlags,
|
|
298
292
|
// '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
299
293
|
'-s', 'FORCE_FILESYSTEM=1',
|
|
300
294
|
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
|
|
301
|
-
...(isProd ? ['-O3'] : []),
|
|
302
295
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
303
296
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
|
|
304
297
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -314,7 +307,10 @@ export default async function buildWasm(target, options = {}) {
|
|
|
314
307
|
], null, target);
|
|
315
308
|
logger.doneStep(target, 'wasm');
|
|
316
309
|
logger.startStep(target, 'js');
|
|
317
|
-
|
|
310
|
+
guardBigIntConversions(target);
|
|
311
|
+
guardArgumentConversions(target);
|
|
312
|
+
separateEmbindCalls(target);
|
|
313
|
+
queueJspiEmbindCalls(target);
|
|
318
314
|
await buildJs(target);
|
|
319
315
|
if (emccFlags.includes('FETCH')) {
|
|
320
316
|
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
@@ -338,5 +334,6 @@ export default async function buildWasm(target, options = {}) {
|
|
|
338
334
|
}
|
|
339
335
|
|
|
340
336
|
fs.writeFileSync(linkFingerprintFile, linkFingerprint);
|
|
337
|
+
writeNativeSourceStamp(`${state.config.paths.build}/${target.jsName}`, state.config.paths.native);
|
|
341
338
|
return true;
|
|
342
339
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CREDENTIALS_FILE, cloudCredentials, readCredentials, writeCredentials,
|
|
3
|
+
} from '../utils/cloudAccount.js';
|
|
4
|
+
|
|
5
|
+
// `crossbind login`, `logout` and `usage`, against tooling/cloud/worker/account-api.js.
|
|
6
|
+
|
|
7
|
+
const GITHUB_ORIGIN = 'https://github.com';
|
|
8
|
+
const TOKEN_PATTERN = /^cbt_[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const MAX_NAME_LENGTH = 64;
|
|
10
|
+
// GitHub's defaults for a device code; it asks for no faster polling than this.
|
|
11
|
+
const MIN_INTERVAL_SECONDS = 5;
|
|
12
|
+
const DEFAULT_EXPIRES_SECONDS = 900;
|
|
13
|
+
|
|
14
|
+
const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
15
|
+
// What the cloud sends ends up on the terminal: no escape sequences.
|
|
16
|
+
const printable = (value) => String(value ?? '').replace(/\p{Cc}/gu, '');
|
|
17
|
+
const paceOf = (seconds, fallback) => Math.max(MIN_INTERVAL_SECONDS, Number.isFinite(seconds) ? seconds : fallback);
|
|
18
|
+
|
|
19
|
+
async function call(fetcher, url, route, { method = 'GET', token, body } = {}) {
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
// A redirect would carry the token to an address nobody configured.
|
|
23
|
+
response = await fetcher(`${url}${route}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
26
|
+
body: body && JSON.stringify(body),
|
|
27
|
+
redirect: 'error',
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`crossbind: crossbind cloud at ${url} is unreachable (${error.cause?.code ?? error.message}).`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
const answer = await response.json().catch(() => null);
|
|
33
|
+
return { status: response.status, ok: response.ok, body: answer !== null && typeof answer === 'object' ? answer : {} };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const refused = ({ status, body }) => new Error(`crossbind: ${printable(body.error ?? `crossbind cloud answered ${status}.`)}`);
|
|
37
|
+
|
|
38
|
+
// { revoked: true }, or { revoked: false, reason }.
|
|
39
|
+
async function revoke(fetcher, url, token, all) {
|
|
40
|
+
try {
|
|
41
|
+
const answer = await call(fetcher, url, all ? '/v1/tokens' : '/v1/token', { method: 'DELETE', token });
|
|
42
|
+
// 401: the cloud no longer knows this token, so there is nothing left to revoke, unless every token was asked for.
|
|
43
|
+
return answer.ok || (!all && answer.status === 401) ? { revoked: true } : { revoked: false, reason: refused(answer).message };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { revoked: false, reason: error.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// GitHub's device flow: the user approves a code on github.com while this polls crossbind cloud, which asks GitHub.
|
|
50
|
+
export async function login({
|
|
51
|
+
url, file = CREDENTIALS_FILE, fetcher = fetch, sleep = wait, now = Date.now, print = console.log,
|
|
52
|
+
}) {
|
|
53
|
+
const device = await call(fetcher, url, '/v1/login/device', { method: 'POST' });
|
|
54
|
+
if (!device.ok) throw refused(device);
|
|
55
|
+
const { deviceCode, userCode, verificationUri } = device.body;
|
|
56
|
+
if (URL.parse(verificationUri)?.origin !== GITHUB_ORIGIN || typeof userCode !== 'string') {
|
|
57
|
+
throw new Error('crossbind: crossbind cloud sent a sign-in page that is not on github.com.');
|
|
58
|
+
}
|
|
59
|
+
print(`crossbind: open ${printable(verificationUri)} and enter the code ${printable(userCode)} to sign in with GitHub.`);
|
|
60
|
+
let pace = paceOf(device.body.interval, MIN_INTERVAL_SECONDS);
|
|
61
|
+
const deadline = now() + (device.body.expiresIn ?? DEFAULT_EXPIRES_SECONDS) * 1000;
|
|
62
|
+
while (now() < deadline) {
|
|
63
|
+
await sleep(pace * 1000);
|
|
64
|
+
const answer = await call(fetcher, url, '/v1/login/token', { method: 'POST', body: { deviceCode } });
|
|
65
|
+
if (answer.status === 202) {
|
|
66
|
+
pace = paceOf(answer.body.interval, pace);
|
|
67
|
+
} else {
|
|
68
|
+
if (!answer.ok) throw refused(answer);
|
|
69
|
+
const { token } = answer.body;
|
|
70
|
+
if (!TOKEN_PATTERN.test(token ?? '')) throw new Error('crossbind: crossbind cloud sent no usable token.');
|
|
71
|
+
const name = printable(answer.body.login).slice(0, MAX_NAME_LENGTH);
|
|
72
|
+
const previous = cloudCredentials(url, file);
|
|
73
|
+
writeCredentials({ ...readCredentials(file), [url]: { token, login: name } }, file);
|
|
74
|
+
if (previous && previous.token !== token) {
|
|
75
|
+
const outcome = await revoke(fetcher, url, previous.token, false);
|
|
76
|
+
if (!outcome.revoked) print(`crossbind: the token this sign-in replaces still works (${outcome.reason.replace(/^crossbind: /, '')}); crossbind logout --all revokes it.`);
|
|
77
|
+
}
|
|
78
|
+
return { login: name };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error('crossbind: the code expired before it was entered; run crossbind login again.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Forgets the token here whatever the cloud answers; `revoked` says whether the cloud dropped it too.
|
|
85
|
+
export async function logout({
|
|
86
|
+
url, all = false, file = CREDENTIALS_FILE, fetcher = fetch,
|
|
87
|
+
}) {
|
|
88
|
+
const { [url]: entry, ...rest } = readCredentials(file);
|
|
89
|
+
if (!entry) return null;
|
|
90
|
+
const outcome = await revoke(fetcher, url, entry.token, all);
|
|
91
|
+
writeCredentials(rest, file);
|
|
92
|
+
return { login: entry.login, ...outcome };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The account, with the sign-in of every machine; the sign-in here is forgotten only once the cloud confirms.
|
|
96
|
+
export async function deleteAccount({ url, file = CREDENTIALS_FILE, fetcher = fetch }) {
|
|
97
|
+
const { [url]: entry, ...rest } = readCredentials(file);
|
|
98
|
+
if (!entry) throw new Error(`crossbind: not signed in to ${url}; run crossbind login first.`);
|
|
99
|
+
const answer = await call(fetcher, url, '/v1/account', { method: 'DELETE', token: entry.token });
|
|
100
|
+
if (answer.status === 401) throw new Error('crossbind: this sign-in is no longer valid; run crossbind login, then delete the account.');
|
|
101
|
+
if (!answer.ok) throw refused(answer);
|
|
102
|
+
writeCredentials(rest, file);
|
|
103
|
+
return { login: entry.login };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function fetchUsage({ url, file = CREDENTIALS_FILE, fetcher = fetch }) {
|
|
107
|
+
const entry = cloudCredentials(url, file);
|
|
108
|
+
if (!entry) throw new Error(`crossbind: not signed in to ${url}; run crossbind login.`);
|
|
109
|
+
const answer = await call(fetcher, url, '/v1/usage', { token: entry.token });
|
|
110
|
+
if (answer.status === 401) throw new Error('crossbind: this sign-in is no longer valid; run crossbind login.');
|
|
111
|
+
if (!answer.ok) throw refused(answer);
|
|
112
|
+
return answer.body;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatSeconds(seconds) {
|
|
116
|
+
const whole = Math.max(0, Math.round(seconds));
|
|
117
|
+
const minutes = Math.floor(whole / 60);
|
|
118
|
+
const rest = whole % 60;
|
|
119
|
+
if (minutes === 0) return `${rest}s`;
|
|
120
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function usageLines({ login: name, builds, playground }) {
|
|
124
|
+
const images = Object.entries(builds.byImage).sort(([, a], [, b]) => b - a);
|
|
125
|
+
const column = Math.max(...images.map(([image]) => image.length)) + 3;
|
|
126
|
+
const left = builds.limitSeconds - builds.usedSeconds;
|
|
127
|
+
return [
|
|
128
|
+
`Signed in as ${printable(name)}.`,
|
|
129
|
+
`Cloud builds in ${printable(builds.month)}: ${formatSeconds(builds.usedSeconds)} of ${formatSeconds(builds.limitSeconds)} used, ${formatSeconds(left)} left.`,
|
|
130
|
+
...images.map(([image, seconds]) => ` ${printable(image).padEnd(column)}${formatSeconds(seconds)}`),
|
|
131
|
+
`Playground: ${playground.remaining} of ${playground.limit} compiles left today.`,
|
|
132
|
+
];
|
|
133
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import createBridgeFile from './createInterface.js';
|
|
5
|
+
import getDependFilePath from '../integration/getDependFilePath.js';
|
|
6
|
+
import resolveNativeImport from '../integration/resolveNativeImport.js';
|
|
7
|
+
import { getRustSymbols } from '../integration/getCrossbindScript.js';
|
|
8
|
+
import { findNativeImportsIn, nativeSpecifierTest } from '../utils/nativeImports.js';
|
|
9
|
+
|
|
10
|
+
const SCHEME = /^(cargo|conan):/;
|
|
11
|
+
const isBare = (specifier) => !specifier.startsWith('.') && !upath.isAbsolute(specifier);
|
|
12
|
+
|
|
13
|
+
function realPath(file) {
|
|
14
|
+
try {
|
|
15
|
+
return upath.normalize(fs.realpathSync(file));
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return upath.resolve(file);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The first target whose packages ship the file. A cargo: or conan: import that none resolves fails the build with
|
|
22
|
+
// what to declare or install, as it does in a bundler.
|
|
23
|
+
function resolveImport(specifier, importer, targets) {
|
|
24
|
+
let failure = null;
|
|
25
|
+
for (const target of targets) {
|
|
26
|
+
try {
|
|
27
|
+
const file = SCHEME.test(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
|
|
28
|
+
if (file && fs.existsSync(file)) return { target, file };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
failure ??= error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (failure) throw failure;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The native files the app's own sources import, by real path, each under the first target whose packages ship it
|
|
38
|
+
// and with the bare specifiers that name it, which Node cannot resolve on its own.
|
|
39
|
+
export function findAppNativeImports(targets) {
|
|
40
|
+
const projectDir = state.config.paths.project;
|
|
41
|
+
const imports = new Map();
|
|
42
|
+
if (!projectDir || !fs.existsSync(projectDir)) return imports;
|
|
43
|
+
for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext))) {
|
|
44
|
+
const resolved = resolveImport(specifier, importer, targets);
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
console.warn(`crossbind: ${upath.relative(projectDir, importer)} imports ${specifier}, which no dependency ships for ${targets.map((t) => t.path).join(', ')}.`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const file = realPath(resolved.file);
|
|
50
|
+
const found = imports.get(file) ?? { file, target: resolved.target, specifiers: [] };
|
|
51
|
+
const specifiers = isBare(specifier) && !found.specifiers.includes(specifier) ? [...found.specifiers, specifier] : found.specifiers;
|
|
52
|
+
imports.set(file, { ...found, specifiers });
|
|
53
|
+
}
|
|
54
|
+
return imports;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The imported files no bridge of the build covers yet, bound the way a bundler plugin binds what it transforms. A Rust
|
|
58
|
+
// import has no bridge file: its crate links through buildAppRustCrates or its package, so it brings its names.
|
|
59
|
+
export function createImportedBridges(bound, imports) {
|
|
60
|
+
const boundFiles = new Set(bound.map(({ file }) => realPath(file)));
|
|
61
|
+
return [...imports.values()].filter(({ file }) => !boundFiles.has(file)).map(({ file, target }) => (file.endsWith('.rs')
|
|
62
|
+
? { file, names: getRustSymbols(file) }
|
|
63
|
+
: { file, bridge: createBridgeFile(file, target) }));
|
|
64
|
+
}
|