crossbind 2.0.0-beta.62 → 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 (107) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +15 -2
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildLib.js +25 -8
  6. package/src/actions/buildNativeCommand.js +84 -0
  7. package/src/actions/buildNode.js +48 -7
  8. package/src/actions/buildTypes.js +8 -2
  9. package/src/actions/buildWasiCommand.js +0 -4
  10. package/src/actions/buildWasm.js +49 -29
  11. package/src/actions/cloudCommands.js +133 -0
  12. package/src/actions/createImportedBridges.js +64 -0
  13. package/src/actions/createInterface.js +247 -68
  14. package/src/actions/createLib.js +57 -41
  15. package/src/actions/getData.js +4 -1
  16. package/src/actions/getLinkInputs.js +12 -5
  17. package/src/actions/isSourceNewer.js +2 -0
  18. package/src/actions/licenses.js +143 -19
  19. package/src/actions/prepareConanDependencies.js +51 -0
  20. package/src/actions/publishBinary.js +32 -0
  21. package/src/actions/run.js +63 -66
  22. package/src/actions/runnerCommands.js +421 -0
  23. package/src/actions/writeRuntimeEntry.js +69 -0
  24. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  25. package/src/assets/cmake/command/command.cpp +1 -0
  26. package/src/assets/cmake/dist.cmake +2 -1
  27. package/src/assets/configure/install.sh +22 -0
  28. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  29. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  30. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -60
  31. package/src/assets/js-runtime/core.js +11 -0
  32. package/src/assets/node-import-hooks.mjs +61 -0
  33. package/src/assets/toolchain-digests.json +19 -19
  34. package/src/assets/wasi-runtime/stubs.c +29 -0
  35. package/src/bin.js +257 -50
  36. package/src/index.js +3 -0
  37. package/src/integration/bindHeaderImports.js +18 -0
  38. package/src/integration/getCrossbindScript.js +46 -10
  39. package/src/integration/getDependFilePath.js +9 -0
  40. package/src/integration/resolveNativeImport.js +14 -0
  41. package/src/node/dev.mjs +19 -0
  42. package/src/runner/blobs.js +135 -0
  43. package/src/runner/files.js +193 -0
  44. package/src/runner/server.js +320 -0
  45. package/src/state/attachConanDependencies.js +56 -0
  46. package/src/state/index.js +17 -9
  47. package/src/state/loadConfig.js +33 -24
  48. package/src/state/refreshBuiltDependencies.js +20 -0
  49. package/src/state/refreshConanDependencies.js +17 -0
  50. package/src/utils/androidToolchain.js +4 -0
  51. package/src/utils/appRustCrates.js +15 -1
  52. package/src/utils/appSources.js +97 -6
  53. package/src/utils/archiveFlags.js +12 -0
  54. package/src/utils/boundHeaders.js +79 -0
  55. package/src/utils/bridgeExtras.js +486 -0
  56. package/src/utils/bridgeRuntimeFixes.js +48 -0
  57. package/src/utils/bundledLicenses.js +26 -0
  58. package/src/utils/cargoTarget.js +21 -3
  59. package/src/utils/cloudAccount.js +55 -0
  60. package/src/utils/conanDependencies.js +66 -0
  61. package/src/utils/conanImport.js +58 -0
  62. package/src/utils/conanInstall.js +154 -0
  63. package/src/utils/conanProfile.js +203 -0
  64. package/src/utils/conanStage.js +292 -0
  65. package/src/utils/copyToDistSource.js +13 -0
  66. package/src/utils/cppDts.js +58 -15
  67. package/src/utils/darwinToolchain.js +11 -0
  68. package/src/utils/dirLock.js +137 -28
  69. package/src/utils/downloadAndExtractFile.js +34 -7
  70. package/src/utils/embindArgumentGuards.js +41 -2
  71. package/src/utils/embindCallArguments.js +19 -0
  72. package/src/utils/embindJspiQueue.js +25 -0
  73. package/src/utils/execContainer.js +49 -0
  74. package/src/utils/headerImports.js +16 -5
  75. package/src/utils/hostPlatform.js +21 -0
  76. package/src/utils/iosToolchain.js +6 -0
  77. package/src/utils/isSourceCmakePackage.js +10 -0
  78. package/src/utils/licenseReport.js +11 -2
  79. package/src/utils/linuxToolchain.js +24 -0
  80. package/src/utils/makeTreeWritable.js +12 -0
  81. package/src/utils/nativeImports.js +23 -0
  82. package/src/utils/nativeSourceStamp.js +36 -0
  83. package/src/utils/nodeDevBuild.js +92 -0
  84. package/src/utils/packageLicense.js +101 -0
  85. package/src/utils/pthreadMainScript.js +14 -0
  86. package/src/utils/pullDockerImage.js +3 -1
  87. package/src/utils/relocatePrebuilt.js +119 -0
  88. package/src/utils/remoteClient.js +127 -0
  89. package/src/utils/remoteOutputs.js +39 -0
  90. package/src/utils/remoteRunner.js +97 -0
  91. package/src/utils/remoteUpload.js +134 -0
  92. package/src/utils/replaceFile.js +8 -0
  93. package/src/utils/runCargo.js +48 -51
  94. package/src/utils/runConan.js +257 -0
  95. package/src/utils/runtimeEntries.js +56 -0
  96. package/src/utils/rustBridgeGen.js +9 -4
  97. package/src/utils/rustMt.js +4 -0
  98. package/src/utils/scopedEmbind.js +22 -0
  99. package/src/utils/selectRunner.js +81 -0
  100. package/src/utils/sourceFingerprint.js +4 -3
  101. package/src/utils/swigInterface.js +69 -4
  102. package/src/utils/systemKeys.js +35 -3
  103. package/src/utils/targets.js +49 -4
  104. package/src/utils/toolchainNotices.js +5 -0
  105. package/src/utils/wasiToolchain.js +14 -1
  106. package/src/utils/windowsToolchain.js +18 -0
  107. package/vitest.config.js +11 -0
package/README.md CHANGED
@@ -255,7 +255,7 @@ OpenSSL · PROJ · SpatiaLite · SQLite · libTIFF · WebP · zlib · Zstandard*
255
255
 
256
256
  Each family has target-specific packages. For example, a browser or Node.js
257
257
  project uses `@crossbind/port-gdal-wasm`, while mobile, native Node.js addon and
258
- WASI builds use the matching `-android`, `-ios`, `-darwin`, `-linux`, `-win32`,
258
+ WASI builds use the matching `-android`, `-ios`, `-darwin`, `-linux`, `-linuxmusl`, `-win32`,
259
259
  or `-wasi` variant. Import the variant's `crossbind.config.js` into the
260
260
  application's dependency list:
261
261
 
@@ -285,7 +285,7 @@ Some upstream command-line tools are also published as WASI-powered npm
285
285
  commands. They require `wasmtime` on `PATH`, but no compiler:
286
286
 
287
287
  ```bash
288
- npm install --global @crossbind/port-gdal-bin-wasi@beta
288
+ npm install --global @crossbind/port-gdal-standalone-wasi@beta
289
289
  gdalinfo-wasi --version
290
290
  ```
291
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.62",
3
+ "version": "2.0.0-beta.63",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": "https://github.com/crossbind/crossbind.git",
@@ -13,6 +13,11 @@
13
13
  "crossbind": "./src/bin.js"
14
14
  },
15
15
  "main": "src/index.js",
16
+ "exports": {
17
+ ".": "./src/index.js",
18
+ "./node/dev": "./src/node/dev.mjs",
19
+ "./*": "./*"
20
+ },
16
21
  "scripts": {
17
22
  "test": "vitest run",
18
23
  "test:watch": "vitest",
@@ -14,6 +14,7 @@ import {
14
14
  } from '../utils/dependencyRebuild.js';
15
15
  import withDirLock from '../utils/dirLock.js';
16
16
  import { prepareRustSysroot } from '../utils/rustSysroot.js';
17
+ import prepareConanDependencies from './prepareConanDependencies.js';
17
18
  import getAbsolutePath from '../utils/getAbsolutePath.js';
18
19
  import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
19
20
  import getParentPath from '../utils/getParentPath.js';
@@ -25,6 +26,7 @@ export default async function buildDependencies({ targetParams, rebuildOption })
25
26
  // sync cargo work downstream reads what it resolves. A project with nothing to rebuild still
26
27
  // needs its sysroot.
27
28
  await prepareRustSysroot(getBuildTargets(targetParams));
29
+ await prepareConanDependencies(getBuildTargets(targetParams));
28
30
  await buildMissingCargoDependencies(getBuildTargets(targetParams), targetParams);
29
31
 
30
32
  const rebuildDeps = getRebuildDeps(state.config.allDependencies, rebuildOption);
@@ -59,6 +61,11 @@ export default async function buildDependencies({ targetParams, rebuildOption })
59
61
  }
60
62
  }
61
63
 
64
+ const isWasm = (target) => target.platform === 'wasm';
65
+ const paramsOf = (target) => ({
66
+ platform: [target.platform], arch: [target.arch], runtime: [target.runtime], buildType: [target.buildType], runtimeEnv: [target.runtimeEnv],
67
+ });
68
+
62
69
  // A cargo dependency carries every target in ONE package, so unlike a platform-split port there
63
70
  // is no "this sibling isn't for this target" case: a missing prebuilt means it was never built.
64
71
  // Nothing else builds these packages - buildDependencies only rebuilds deps that carry a source
@@ -66,11 +73,16 @@ export default async function buildDependencies({ targetParams, rebuildOption })
66
73
  // pre-build step nobody had written down. Worse, only the cmake link asserts the dependency is
67
74
  // there: on wasm a missing one silently drops out and the module builds clean, then dies at init.
68
75
  // Build it here instead, in the dependency's own package, where its prebuilt belongs.
69
- async function buildMissingCargoDependencies(targets, targetParams) {
70
- if (targets.length === 0) return;
76
+ async function buildMissingCargoDependencies(buildTargets, targetParams) {
77
+ if (buildTargets.length === 0) return;
71
78
  const appConfig = state.config;
72
79
  const cargoDependencies = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig && dep.export?.type === 'cargo');
73
80
  if (cargoDependencies.length === 0) return;
81
+ // Bridges read every header for the first wasm target (see prepareConanDependencies), which an app configured for
82
+ // another wasm runtime does not build: there a dependency built for the app's runtime alone counts as half built.
83
+ const bridgeTarget = buildTargets.some(isWasm) ? state.targets.find(isWasm) : undefined;
84
+ const extraTarget = bridgeTarget && !buildTargets.some((target) => target.path === bridgeTarget.path) ? bridgeTarget : undefined;
85
+ const targets = extraTarget ? [...buildTargets, extraTarget] : buildTargets;
74
86
  // A prebuilt from another embind-rs counts as missing: linked, it carries stale glue or clashes with the others.
75
87
  const fingerprint = getEmbindRsFingerprint();
76
88
  const prebuiltOf = (dep, target) => [target.path, target.releasePath]
@@ -91,6 +103,7 @@ async function buildMissingCargoDependencies(targets, targetParams) {
91
103
  state.config = scoped;
92
104
  try {
93
105
  buildLib(targetParams);
106
+ if (extraTarget) buildLib(paramsOf(extraTarget));
94
107
  } finally {
95
108
  state.config = prev;
96
109
  }
@@ -1,10 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import replace from 'replace';
3
3
  import downloadAndExtractFile from '../utils/downloadAndExtractFile.js';
4
+ import copyToDistSource from '../utils/copyToDistSource.js';
4
5
  import { getBuildTargets } from './target.js';
5
6
  import state from '../state/index.js';
6
7
  import buildLib from './buildLib.js';
7
8
  import buildBinTools from './buildBinTools.js';
9
+ import { copyBundledLicenses } from '../utils/bundledLicenses.js';
8
10
 
9
11
  export default async function buildExternal(targetParams, options = {}) {
10
12
  const version = state.config.package.nativeVersion;
@@ -31,6 +33,12 @@ export default async function buildExternal(targetParams, options = {}) {
31
33
 
32
34
  buildLib(targetParams, options);
33
35
 
36
+ const { bundled } = state.config.build;
37
+ if (bundled) {
38
+ new Set(getBuildTargets(targetParams).map((target) => target.platform))
39
+ .forEach((platform) => copyBundledLicenses(bundled[platform], sourcePath, state.config.paths.output));
40
+ }
41
+
34
42
  if (state.config.build.bin) {
35
43
  await buildBinTools(targetParams);
36
44
  }
@@ -48,7 +56,7 @@ export default async function buildExternal(targetParams, options = {}) {
48
56
  targets.forEach(target => {
49
57
  const assetPath = `${state.config.paths.output}/prebuilt/${target.path}/${v}`;
50
58
  if (!fs.existsSync(assetPath)) {
51
- fs.copyFileSync(`${state.config.paths.project}/${key}`, assetPath);
59
+ fs.copyFileSync(copyToDistSource(state.config.paths.project, key), assetPath);
52
60
  }
53
61
  });
54
62
  });
@@ -8,6 +8,9 @@ import logger from '../utils/logger.js';
8
8
  import findFiles from '../utils/findFiles.js';
9
9
  import { getSourceFingerprint, isSourceFingerprintStale, staleTargetDirectories, writeSourceFingerprint } from '../utils/sourceFingerprint.js';
10
10
  import { getEmbindRsFingerprint, writeEmbindRsFingerprint } from '../utils/embindRsFingerprint.js';
11
+ import relocatePrebuilt from '../utils/relocatePrebuilt.js';
12
+ import replaceBasePathForDocker, { DOCKER_BASE } from '../utils/replaceBasePathForDocker.js';
13
+ import { writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
11
14
 
12
15
  export default function buildLib(targetParams, options = {}) {
13
16
  let isChanged = false;
@@ -22,9 +25,10 @@ export default function buildLib(targetParams, options = {}) {
22
25
  // existence-only skip below would keep serving a stale staged staticlib after source edits.
23
26
  const isCargo = state.config.export?.type === 'cargo';
24
27
  const libdir = `${state.config.paths.output}/prebuilt/${target.path}`;
25
- const sourceFingerprint = getSourceFingerprint(state.config);
28
+ const sourceFingerprint = getSourceFingerprint(state.config, target);
26
29
  const sourceChanged = isSourceFingerprintStale(libdir, sourceFingerprint);
27
- if (isCargo || sourceChanged || !fs.existsSync(`${libdir}/lib`)) {
30
+ const nativeChanged = isNativeSourceNewerThan(`${libdir}/lib`);
31
+ if (isCargo || sourceChanged || nativeChanged || !fs.existsSync(`${libdir}/lib`)) {
28
32
  if (sourceChanged) {
29
33
  for (const stale of staleTargetDirectories({
30
34
  buildPath: state.config.paths.build,
@@ -33,8 +37,9 @@ export default function buildLib(targetParams, options = {}) {
33
37
  })) fs.rmSync(stale, { recursive: true, force: true });
34
38
  }
35
39
  // createLib caches on existence too, so re-entering is not enough: a stale stamp means
36
- // the lib on disk came from a different upstream release and has to be rebuilt.
37
- createLib(target, 'Source', { buildSource: true, force: sourceChanged });
40
+ // the lib on disk came from a different upstream release, and a newer native source means
41
+ // it predates an edit; either way it has to be rebuilt.
42
+ createLib(target, 'Source', { buildSource: true, force: sourceChanged || nativeChanged });
38
43
 
39
44
  const modules = [];
40
45
  state.config.paths.module.forEach((modulePath) => {
@@ -52,10 +57,6 @@ export default function buildLib(targetParams, options = {}) {
52
57
  if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint());
53
58
  isChanged = true;
54
59
  } else {
55
- // The skip is existence-only; without this warning a source edit is served stale silently.
56
- if (isNativeSourceNewerThan(`${state.config.paths.output}/prebuilt/${target.path}/lib`)) {
57
- logger.info(`[${target.path}] lib cached but native sources are newer - delete the prebuilt output (rm -rf dist .crossbind) and rebuild to pick them up`);
58
- }
59
60
  logger.cachedStep(target, 'lib');
60
61
  }
61
62
  });
@@ -66,6 +67,11 @@ export default function buildLib(targetParams, options = {}) {
66
67
  if (isChanged && fs.existsSync(`${state.config.paths.build}/Source-Debug/prebuilt`)) {
67
68
  fs.cpSync(`${state.config.paths.build}/Source-Debug/prebuilt`, `${state.config.paths.output}/prebuilt`, { recursive: true, dereference: true });
68
69
  }
70
+ if (isChanged) targets.forEach((target) => {
71
+ writeNativeSourceStamp(`${state.config.paths.output}/prebuilt/${target.path}/lib`, state.config.paths.native);
72
+ });
73
+ // Cached trees too, so a rebuild fixes a prebuilt made before relocation existed.
74
+ targets.forEach(relocateTargetPrebuilt);
69
75
 
70
76
  if (!options.skipXcframework) {
71
77
  createXCFramework();
@@ -129,3 +135,14 @@ export default function buildLib(targetParams, options = {}) {
129
135
  fs.writeFileSync(`${state.config.paths.output}/prebuilt/CMakeLists.txt`, distCmakeContent);
130
136
  }
131
137
  }
138
+
139
+ function relocateTargetPrebuilt(target) {
140
+ const { base, build, output } = state.config.paths;
141
+ const prefixDir = `${output}/prebuilt/${target.path}`;
142
+ if (!fs.existsSync(prefixDir)) return;
143
+ const installPrefix = `${build}/Source-${target.buildType === 'release' ? 'Release' : 'Debug'}/prebuilt/${target.path}`;
144
+ relocatePrebuilt(prefixDir, {
145
+ installPrefixes: [installPrefix, replaceBasePathForDocker(installPrefix, base)],
146
+ buildBases: [base, DOCKER_BASE],
147
+ });
148
+ }
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { fileURLToPath } from 'node:url';
4
+ import run from './run.js';
5
+ import getLinkInputs from './getLinkInputs.js';
6
+ import getData from './getData.js';
7
+ import state from '../state/index.js';
8
+ import logger from '../utils/logger.js';
9
+ import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
10
+ import { buildLinkLibArgs } from '../utils/linkLayout.js';
11
+
12
+ const cpuCount = Math.max(1, os.cpus().length - 1);
13
+ const PROJECT_SOURCE = fileURLToPath(new URL('../assets/cmake/command', import.meta.url));
14
+ const PROJECT_FILES = ['CMakeLists.txt', 'command.cpp'];
15
+
16
+ const statOf = (lib) => {
17
+ const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
18
+ return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
19
+ };
20
+
21
+ // The CLI may sit outside the docker mount, so the project is copied next to the build.
22
+ function stageProject(build) {
23
+ const projectDir = `${build}/crossbind-command`;
24
+ fs.mkdirSync(projectDir, { recursive: true });
25
+ PROJECT_FILES.forEach((file) => fs.copyFileSync(`${PROJECT_SOURCE}/${file}`, `${projectDir}/${file}`));
26
+ return projectDir;
27
+ }
28
+
29
+ function linkInputs(target) {
30
+ const { libs, wholeArchiveAll, wholeArchiveNames } = getLinkInputs(target, { keepFlag: () => '', withBridge: false });
31
+ const linkArgs = [
32
+ ...buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames, forceLoad: target.platform === 'darwin' }),
33
+ // The system libraries the archives need, declared by the packages that bring them in.
34
+ ...(getData('binary', target)?.addonFlags ?? []),
35
+ ];
36
+ return { libs, linkArgs };
37
+ }
38
+
39
+ // Links the project's main() and the archives it reaches into one executable.
40
+ export default async function buildNativeCommand(target, options = {}) {
41
+ if (target.platform === 'darwin' && process.platform !== 'darwin') {
42
+ logger.info(`[${target.path}] native command skipped (macOS executables link on a macOS host)`);
43
+ return false;
44
+ }
45
+ if (state.config.export.type === 'cargo') {
46
+ logger.info(`[${target.path}] native command skipped (cargo package - staticlib only)`);
47
+ return false;
48
+ }
49
+
50
+ const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
51
+ const { build } = state.config.paths;
52
+ const { libs, linkArgs } = linkInputs(target);
53
+ const isStatic = target.platform === 'linuxmusl';
54
+
55
+ const fingerprintFile = `${build}/${target.commandName}.fingerprint`;
56
+ const fingerprint = getContentHash(JSON.stringify({
57
+ builder: getFilesFingerprint([fileURLToPath(import.meta.url), ...PROJECT_FILES.map((file) => `${PROJECT_SOURCE}/${file}`)]),
58
+ linkArgs,
59
+ isStatic,
60
+ libs: libs.map(statOf),
61
+ }));
62
+ const isLinkChanged = !fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, 'utf8') !== fingerprint;
63
+ if (!options.force && !isLinkChanged && fs.existsSync(`${build}/${target.commandName}`)) {
64
+ logger.cachedStep(target, 'native command');
65
+ return false;
66
+ }
67
+
68
+ const platformPrefix = `Native-${buildType}`;
69
+ logger.startStep(target, 'native command');
70
+ run(null, [
71
+ 'cmake', stageProject(build),
72
+ `-DCMAKE_BUILD_TYPE=${buildType}`,
73
+ `-DCROSSBIND_COMMAND_FILE=${target.commandName}`,
74
+ `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
75
+ `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
76
+ `-DCROSSBIND_STATIC=${isStatic ? 'ON' : 'OFF'}`,
77
+ ], platformPrefix, target);
78
+ run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
79
+ fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.commandName}`, `${build}/${target.commandName}`);
80
+ logger.doneStep(target, 'native command');
81
+
82
+ fs.writeFileSync(fingerprintFile, fingerprint);
83
+ return true;
84
+ }
@@ -14,9 +14,13 @@ import logger from '../utils/logger.js';
14
14
  import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
15
15
  import { buildLinkLibArgs } from '../utils/linkLayout.js';
16
16
  import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
17
+ import scopedEmbind from '../utils/scopedEmbind.js';
17
18
  import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
19
+ import { rustStdLibsFor } from '../utils/cargoTarget.js';
20
+ import toolchainNoticesDir from '../utils/toolchainNotices.js';
18
21
 
19
22
  const cpuCount = Math.max(1, os.cpus().length - 1);
23
+ const WINDOWS_IMAGE_NOTICES = '/opt/licenses/llvm-mingw';
20
24
 
21
25
  function filesUnder(dirs) {
22
26
  return dirs.flatMap((dir) => (fs.existsSync(dir)
@@ -28,16 +32,20 @@ function filesUnder(dirs) {
28
32
 
29
33
  function linkInputs(target) {
30
34
  const {
31
- libs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
35
+ libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
32
36
  } = getLinkInputs(target, {
33
37
  // The linker loads the archive member that defines a -u symbol; Mach-O prefixes C names with _.
34
38
  keepFlag: (name) => `-Wl,-u,${target.platform === 'darwin' ? '_' : ''}crossbind_keep_${name}`,
35
39
  });
40
+ // The app's Rust goes first: what a package's kept member needs from std and rustc's allocator shims is then
41
+ // already the app's, and the package's own copies stay in its archive instead of defining them twice.
42
+ const ordered = [...appRustLibs, ...libs.filter((lib) => !appRustLibs.includes(lib))];
36
43
  const linkArgs = [
37
- ...buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames, forceLoad: target.platform === 'darwin' }),
44
+ ...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad: target.platform === 'darwin' }),
38
45
  ...rustKeepFlags,
39
46
  // The system libraries the archives need, declared by the packages that bring them in.
40
47
  ...(getData('binary', target)?.addonFlags ?? []),
48
+ ...(hasRust ? rustStdLibsFor(target) : []),
41
49
  ];
42
50
  // The addon runs embind-jsi, so Rust packages bind through the adapter React Native links.
43
51
  const extraSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/jsi.cpp`] : [];
@@ -49,14 +57,34 @@ function envOf(target) {
49
57
  .map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
50
58
  }
51
59
 
60
+ // A package publishes its addons in <package>-<platform>-<arch> by listing them as optional
61
+ // dependencies, which is also how npm learns to install only the one for its machine.
62
+ function listsAddonPackage(target) {
63
+ const { name, optionalDependencies = {} } = state.config.package ?? {};
64
+ return Boolean(name) && Object.hasOwn(optionalDependencies, `${name}-${target.platform}-${target.arch}`);
65
+ }
66
+
67
+ function addonPackagePattern(served) {
68
+ return served.some(listsAddonPackage) ? `${state.config.package.name}-{platform}-{arch}` : null;
69
+ }
70
+
71
+ // Each of those packages links its own addon, so a package that lists any links none. The ones it lists are
72
+ // the platforms it publishes for.
73
+ export function addonsInPackages() {
74
+ return state.targets.some(listsAddonPackage);
75
+ }
76
+
52
77
  // One loader serves every addon of a build type, so each addon's env is keyed the way the loader
53
- // looks it up at runtime rather than taken from whichever arch happened to build last.
78
+ // looks it up at runtime rather than taken from whichever arch happened to build last. With its addons in
79
+ // packages, it knows the addons the package publishes.
54
80
  export function nodeLoaderConfig(target) {
55
81
  const served = state.targets.filter((t) => t.addonPattern && t.jsName === target.jsName);
82
+ const addonPackage = addonPackagePattern(served);
83
+ const loaded = addonPackage ? served.filter(listsAddonPackage) : served;
56
84
  return {
57
- env: Object.fromEntries(served.map((t) => [`${t.platform}-${t.arch}`, envOf(t)])),
85
+ env: Object.fromEntries(loaded.map((t) => [`${t.platform}-${t.arch}`, envOf(t)])),
58
86
  general: { name: state.config.general.name },
59
- paths: { addon: target.addonPattern },
87
+ paths: { addon: target.addonPattern, ...(addonPackage ? { addonPackage } : {}) },
60
88
  };
61
89
  }
62
90
 
@@ -77,13 +105,20 @@ async function bundleLoader(target, napiRoot, loaderConfig) {
77
105
  const systemConfig = `export default ${JSON.stringify(loaderConfig)};`;
78
106
  const bundle = await rollup({
79
107
  input: `${napiRoot}/js/loader.js`,
80
- plugins: [virtual({ 'crossbind/systemConfig': systemConfig }), nodeResolve(), commonjs()],
108
+ plugins: [
109
+ virtual({ 'crossbind/systemConfig': systemConfig }), scopedEmbind(`${resolveEmbindJsiRoot()}/js/embind.js`), nodeResolve(), commonjs(),
110
+ ],
81
111
  onwarn: suppressNodeBuiltinWarnings,
82
112
  });
83
113
  await bundle.write({ file: `${state.config.paths.build}/${target.jsName}`, format: 'cjs', exports: 'default' });
84
114
  await bundle.close();
85
115
  }
86
116
 
117
+ // The loader alone, for a package whose platform packages link its addons.
118
+ export function bundleNodeLoader(target) {
119
+ return bundleLoader(target, resolveEmbindNapiRoot(), nodeLoaderConfig(target));
120
+ }
121
+
87
122
  // Links one Node-API addon from the project's archives and bundles the loader that picks the
88
123
  // addon matching the running process.
89
124
  export default async function buildNode(target, options = {}) {
@@ -136,7 +171,13 @@ export default async function buildNode(target, options = {}) {
136
171
  `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
137
172
  `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
138
173
  ], platformPrefix, target);
139
- run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target);
174
+ run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
175
+ if (target.platform === 'win32') {
176
+ // The mingw-w64 runtime and winpthreads in the addon ask for their notices in binary
177
+ // distributions. The windows image carries them; a host toolchain may not.
178
+ const notices = toolchainNoticesDir('win32');
179
+ run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
180
+ }
140
181
  fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
141
182
  logger.doneStep(target, 'addon');
142
183
 
@@ -9,7 +9,13 @@ import logger from '../utils/logger.js';
9
9
  // Package type publishing (opt-in: config `types: true`): one combined .d.ts over every
10
10
  // public header, written to <output>/types/index.d.ts, and package.json wired the way
11
11
  // hand-maintained typings already are - `types` plus a greedy `typesVersions` "*.h" map -
12
- // so a consumer's `import ... from '<pkg>/<any>.h'` resolves to the generated file.
12
+ // so a consumer's `import ... from '<pkg>/<any>.h'` resolves to the generated file. The other
13
+ // paths a package maps, such as its node/napi entry, stay.
14
+ export function withPackageTypes(manifest, relative) {
15
+ const versions = manifest.typesVersions ?? {};
16
+ return { ...manifest, types: relative, typesVersions: { ...versions, '*': { ...versions['*'], '*.h': [relative] } } };
17
+ }
18
+
13
19
  export default function buildTypes() {
14
20
  if (state.config.types !== true) return;
15
21
  const exts = state.config.ext.header.join(',');
@@ -25,7 +31,7 @@ export default function buildTypes() {
25
31
  const manifestFile = `${state.config.paths.project}/package.json`;
26
32
  const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
27
33
  const relative = `./${path.relative(state.config.paths.project, dtsFile)}`;
28
- const next = { ...manifest, types: relative, typesVersions: { '*': { '*.h': [relative] } } };
34
+ const next = withPackageTypes(manifest, relative);
29
35
  if (JSON.stringify({ t: next.types, v: next.typesVersions }) !== JSON.stringify({ t: manifest.types, v: manifest.typesVersions })) {
30
36
  fs.writeFileSync(manifestFile, `${JSON.stringify(next, null, 4)}\n`);
31
37
  }
@@ -13,10 +13,6 @@ export default async function buildWasiCommand(target, options = {}) {
13
13
  const isProd = target.buildType === 'release';
14
14
  const buildType = isProd ? 'Release' : 'Debug';
15
15
 
16
- if (state.config.export.bundle === false) {
17
- logger.info(`[${target.path}] wasi command skipped (export.bundle = false)`);
18
- return false;
19
- }
20
16
  if (state.config.export?.type === 'cargo') {
21
17
  logger.info(`[${target.path}] wasi command skipped (cargo packages have no cargo wasi triple)`);
22
18
  return false;
@@ -1,6 +1,6 @@
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
5
  import getLinkInputs from './getLinkInputs.js';
6
6
  import getData from './getData.js';
@@ -12,6 +12,12 @@ import logger from '../utils/logger.js';
12
12
  import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
13
13
  import { buildLinkLibArgs } from '../utils/linkLayout.js';
14
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
@@ -36,15 +42,41 @@ function guardArgumentConversions(target) {
36
42
  }
37
43
  }
38
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
+
39
77
  export default async function buildWasm(target, options = {}) {
40
78
  const isProd = target.buildType === 'release';
41
79
 
42
- // Caller can opt out of the final emcc link entirely (e.g. when the
43
- // package is consumed only as a static library by downstream builds).
44
- if (state.config.export.bundle === false) {
45
- logger.info(`[${target.path}] wasm+js skipped (export.bundle = false)`);
46
- return false;
47
- }
48
80
  // Cargo packages only stage their staticlib; the consuming app links and bundles it.
49
81
  if (state.config.export.type === 'cargo') {
50
82
  logger.info(`[${target.path}] wasm+js skipped (cargo package - staticlib only)`);
@@ -131,7 +163,8 @@ export default async function buildWasm(target, options = {}) {
131
163
  // The emcc arg tail is hardcoded per environment in this file, invisible to the
132
164
  // emccFlags entry below - hashing the builder itself makes any inline-arg edit
133
165
  // (a new -s flag, an EXPORTED_RUNTIME_METHODS change) a guaranteed cache miss.
134
- 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]),
135
168
  wholeArchiveAll,
136
169
  wholeArchiveNames: [...wholeArchiveNames].sort(),
137
170
  emccFlags,
@@ -188,27 +221,7 @@ export default async function buildWasm(target, options = {}) {
188
221
  const t1 = performance.now();
189
222
  logger.doneStep(target, 'wasm');
190
223
  logger.startStep(target, 'js');
191
- // The pthread bootstrap spawns workers from _scriptName, captured from
192
- // document.currentScript at LOAD time - undefined when the bridge is
193
- // loaded as a module script, so workers would fetch "/undefined" and
194
- // direct-mode mt init hangs. Inject Module.crossbindMainScript (set by the
195
- // browser adapter from paths.worker/paths.js; a crossbind-specific key, as
196
- // emscripten's own mainScriptUrlOrBlob trips emsdk 6 debug assertions
197
- // on st targets) at SPAWN time instead. The previous spaced
198
- // 'var _scriptName = ' rewrite no longer matched the minified glue and
199
- // silently no-opped; the guard below turns any future format drift
200
- // into a visible error instead.
201
- replace({
202
- regex: 'pthreadMainJs=_scriptName',
203
- replacement: 'pthreadMainJs=Module["crossbindMainScript"]||_scriptName',
204
- paths: [`${state.config.paths.build}/${target.rawJsName}`],
205
- recursive: false,
206
- silent: true,
207
- });
208
- const glue = fs.readFileSync(`${state.config.paths.build}/${target.rawJsName}`, 'utf8');
209
- if (glue.includes('pthreadMainJs') && !glue.includes('Module["crossbindMainScript"]||_scriptName')) {
210
- logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
211
- }
224
+ pointPthreadSpawn(target);
212
225
  /* replace({
213
226
  regex: 'val === 10',
214
227
  replacement: 'false',
@@ -218,6 +231,8 @@ export default async function buildWasm(target, options = {}) {
218
231
  }); */
219
232
  guardBigIntConversions(target);
220
233
  guardArgumentConversions(target);
234
+ separateEmbindCalls(target);
235
+ queueJspiEmbindCalls(target);
221
236
  await buildJs(target);
222
237
  // fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
223
238
  // fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
@@ -258,6 +273,8 @@ export default async function buildWasm(target, options = {}) {
258
273
  logger.startStep(target, 'js');
259
274
  guardBigIntConversions(target);
260
275
  guardArgumentConversions(target);
276
+ separateEmbindCalls(target);
277
+ queueJspiEmbindCalls(target);
261
278
  await buildJs(target);
262
279
  logger.doneStep(target, 'js');
263
280
  }
@@ -292,6 +309,8 @@ export default async function buildWasm(target, options = {}) {
292
309
  logger.startStep(target, 'js');
293
310
  guardBigIntConversions(target);
294
311
  guardArgumentConversions(target);
312
+ separateEmbindCalls(target);
313
+ queueJspiEmbindCalls(target);
295
314
  await buildJs(target);
296
315
  if (emccFlags.includes('FETCH')) {
297
316
  fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
@@ -315,5 +334,6 @@ export default async function buildWasm(target, options = {}) {
315
334
  }
316
335
 
317
336
  fs.writeFileSync(linkFingerprintFile, linkFingerprint);
337
+ writeNativeSourceStamp(`${state.config.paths.build}/${target.jsName}`, state.config.paths.native);
318
338
  return true;
319
339
  }