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.
Files changed (118) hide show
  1. package/README.md +8 -6
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +37 -9
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildJs.js +1 -1
  6. package/src/actions/buildLib.js +27 -8
  7. package/src/actions/buildNativeCommand.js +84 -0
  8. package/src/actions/buildNode.js +190 -0
  9. package/src/actions/buildTypes.js +8 -2
  10. package/src/actions/buildWasiCommand.js +0 -4
  11. package/src/actions/buildWasm.js +92 -95
  12. package/src/actions/cloudCommands.js +133 -0
  13. package/src/actions/createImportedBridges.js +64 -0
  14. package/src/actions/createInterface.js +318 -65
  15. package/src/actions/createLib.js +60 -43
  16. package/src/actions/getAllBridges.js +1 -1
  17. package/src/actions/getData.js +4 -1
  18. package/src/actions/getLinkInputs.js +70 -0
  19. package/src/actions/isSourceNewer.js +2 -0
  20. package/src/actions/licenses.js +175 -17
  21. package/src/actions/prepareConanDependencies.js +51 -0
  22. package/src/actions/publishBinary.js +32 -0
  23. package/src/actions/run.js +124 -32
  24. package/src/actions/runnerCommands.js +421 -0
  25. package/src/actions/writeRuntimeEntry.js +69 -0
  26. package/src/assets/cmake/CMakeLists.txt +1 -1
  27. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  28. package/src/assets/cmake/command/command.cpp +1 -0
  29. package/src/assets/cmake/dist.cmake +2 -1
  30. package/src/assets/configure/install.sh +22 -0
  31. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  32. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  33. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
  34. package/src/assets/js-runtime/core.js +11 -0
  35. package/src/assets/node-import-hooks.mjs +61 -0
  36. package/src/assets/toolchain-digests.json +27 -13
  37. package/src/assets/wasi-runtime/stubs.c +29 -0
  38. package/src/bin.js +290 -37
  39. package/src/index.js +6 -1
  40. package/src/integration/bindHeaderImports.js +18 -0
  41. package/src/integration/getCrossbindScript.js +53 -19
  42. package/src/integration/getDependFilePath.js +18 -12
  43. package/src/integration/resolveNativeImport.js +14 -0
  44. package/src/node/dev.mjs +19 -0
  45. package/src/runner/blobs.js +135 -0
  46. package/src/runner/files.js +193 -0
  47. package/src/runner/server.js +320 -0
  48. package/src/state/attachConanDependencies.js +56 -0
  49. package/src/state/index.js +31 -20
  50. package/src/state/loadConfig.js +36 -24
  51. package/src/state/refreshBuiltDependencies.js +20 -0
  52. package/src/state/refreshConanDependencies.js +17 -0
  53. package/src/utils/androidToolchain.js +4 -0
  54. package/src/utils/appRustCrates.js +17 -1
  55. package/src/utils/appSources.js +112 -0
  56. package/src/utils/archiveFlags.js +12 -0
  57. package/src/utils/boundHeaders.js +79 -0
  58. package/src/utils/bridgeExtras.js +486 -0
  59. package/src/utils/bridgeRuntimeFixes.js +48 -0
  60. package/src/utils/bundledLicenses.js +26 -0
  61. package/src/utils/cargoImport.js +44 -0
  62. package/src/utils/cargoTarget.js +23 -3
  63. package/src/utils/cloudAccount.js +55 -0
  64. package/src/utils/conanDependencies.js +66 -0
  65. package/src/utils/conanImport.js +58 -0
  66. package/src/utils/conanInstall.js +154 -0
  67. package/src/utils/conanProfile.js +203 -0
  68. package/src/utils/conanStage.js +292 -0
  69. package/src/utils/copyToDistSource.js +13 -0
  70. package/src/utils/cppDts.js +134 -55
  71. package/src/utils/darwinToolchain.js +11 -0
  72. package/src/utils/dirLock.js +137 -28
  73. package/src/utils/downloadAndExtractFile.js +34 -7
  74. package/src/utils/embindArgumentGuards.js +84 -0
  75. package/src/utils/embindCallArguments.js +19 -0
  76. package/src/utils/embindJspiQueue.js +25 -0
  77. package/src/utils/embindRsFingerprint.js +26 -0
  78. package/src/utils/execContainer.js +49 -0
  79. package/src/utils/headerImports.js +56 -0
  80. package/src/utils/hostPlatform.js +21 -0
  81. package/src/utils/inputStamp.js +23 -0
  82. package/src/utils/iosToolchain.js +6 -0
  83. package/src/utils/isSourceCmakePackage.js +10 -0
  84. package/src/utils/licenseReport.js +11 -2
  85. package/src/utils/linkLayout.js +8 -1
  86. package/src/utils/linuxToolchain.js +24 -0
  87. package/src/utils/makeTreeWritable.js +12 -0
  88. package/src/utils/nativeImports.js +23 -0
  89. package/src/utils/nativeSourceStamp.js +36 -0
  90. package/src/utils/nodeDevBuild.js +92 -0
  91. package/src/utils/packageLicense.js +101 -0
  92. package/src/utils/pthreadMainScript.js +14 -0
  93. package/src/utils/pullDockerImage.js +10 -5
  94. package/src/utils/relocatePrebuilt.js +119 -0
  95. package/src/utils/remoteClient.js +127 -0
  96. package/src/utils/remoteOutputs.js +39 -0
  97. package/src/utils/remoteRunner.js +97 -0
  98. package/src/utils/remoteUpload.js +134 -0
  99. package/src/utils/replaceFile.js +8 -0
  100. package/src/utils/resolveCompanionPackage.js +51 -0
  101. package/src/utils/resolveEmbindNapi.js +22 -0
  102. package/src/utils/resolveEmbindRust.js +17 -52
  103. package/src/utils/runCargo.js +51 -53
  104. package/src/utils/runConan.js +257 -0
  105. package/src/utils/runtimeEntries.js +56 -0
  106. package/src/utils/rustBridgeGen.js +903 -253
  107. package/src/utils/rustMt.js +4 -0
  108. package/src/utils/scopedEmbind.js +22 -0
  109. package/src/utils/selectRunner.js +81 -0
  110. package/src/utils/sourceFingerprint.js +4 -3
  111. package/src/utils/swigInterface.js +86 -6
  112. package/src/utils/systemKeys.js +43 -3
  113. package/src/utils/targets.js +104 -3
  114. package/src/utils/toolchainNotices.js +5 -0
  115. package/src/utils/wasiToolchain.js +14 -1
  116. package/src/utils/windowsToolchain.js +18 -0
  117. package/vitest.config.js +11 -0
  118. package/src/utils/cppFieldBindings.js +0 -23
@@ -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 getDependLibs from './getDependLibs.js';
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 buildAppRustCrates from '../utils/appRustCrates.js';
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 guardBigIntArguments(target) {
25
+ function guardBigIntConversions(target) {
20
26
  const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
21
- replace({
22
- regex: 'if\\s*\\(\\s*typeof value\\s*==\\s*"number"\\s*\\)\\s*\\{\\s*value\\s*=\\s*BigInt\\(value\\)\\s*;?\\s*\\}',
23
- replacement: 'if(typeof value=="number"){if(!Number.isSafeInteger(value))throw new TypeError("a 64-bit integer parameter takes a BigInt or a safe integer Number, got "+value);value=BigInt(value)}',
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
- // buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
51
- // build-dir copy can be gone while the output artifact is still valid — link it then.
52
- const sourceLibCandidates = [
53
- `${state.config.paths.build}/Source-${buildType}/${target.path}/lib${state.config.general.name}.a`,
54
- `${state.config.paths.output}/prebuilt/${target.path}/lib/lib${state.config.general.name}.a`,
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: getFilesFingerprint([fileURLToPath(import.meta.url)]),
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
- // The pthread bootstrap spawns workers from _scriptName, captured from
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
- guardBigIntArguments(target);
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
- guardBigIntArguments(target);
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
- guardBigIntArguments(target);
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
+ }