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
package/README.md CHANGED
@@ -68,8 +68,9 @@ target-specific artifacts, transitive dependencies, build recipes, and runtime
68
68
  adapters.
69
69
 
70
70
  - **Target-aware distribution.** A package family can carry WebAssembly, iOS,
71
- Android, and WASI variants; each build consumes only the artifact valid for
72
- its platform, architecture, runtime, environment, and build type.
71
+ Android, macOS, Linux, Windows, and WASI variants; each build consumes only the
72
+ artifact valid for its platform, architecture, runtime, environment, and build
73
+ type.
73
74
  - **Native dependency resolution.** Package manifests and crossbind configs
74
75
  carry the transitive native graph, so prerequisites are built and linked in
75
76
  the correct order.
@@ -253,9 +254,10 @@ The repository contains 16 port families:
253
254
  OpenSSL · PROJ · SpatiaLite · SQLite · libTIFF · WebP · zlib · Zstandard**
254
255
 
255
256
  Each family has target-specific packages. For example, a browser or Node.js
256
- project uses `@crossbind/port-gdal-wasm`, while mobile and WASI builds use the
257
- matching `-android`, `-ios`, or `-wasi` variant. Import the variant's
258
- `crossbind.config.js` into the application's dependency list:
257
+ project uses `@crossbind/port-gdal-wasm`, while mobile, native Node.js addon and
258
+ WASI builds use the matching `-android`, `-ios`, `-darwin`, `-linux`, `-linuxmusl`, `-win32`,
259
+ or `-wasi` variant. Import the variant's `crossbind.config.js` into the
260
+ application's dependency list:
259
261
 
260
262
  ```bash
261
263
  npm install @crossbind/port-gdal-wasm@beta
@@ -283,7 +285,7 @@ Some upstream command-line tools are also published as WASI-powered npm
283
285
  commands. They require `wasmtime` on `PATH`, but no compiler:
284
286
 
285
287
  ```bash
286
- npm install --global @crossbind/port-gdal-bin-wasi@beta
288
+ npm install --global @crossbind/port-gdal-standalone-wasi@beta
287
289
  gdalinfo-wasi --version
288
290
  ```
289
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.60",
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,12 +14,19 @@ 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';
18
+ import getAbsolutePath from '../utils/getAbsolutePath.js';
19
+ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
20
+ import getParentPath from '../utils/getParentPath.js';
21
+ import calculateDependencyParameters from '../state/calculateDependencyParameters.js';
22
+ import { getEmbindRsFingerprint, isEmbindRsFingerprintStale } from '../utils/embindRsFingerprint.js';
17
23
 
18
24
  export default async function buildDependencies({ targetParams, rebuildOption }) {
19
25
  // Before the early returns below: this is the one async step every build path awaits, and the
20
26
  // sync cargo work downstream reads what it resolves. A project with nothing to rebuild still
21
27
  // needs its sysroot.
22
28
  await prepareRustSysroot(getBuildTargets(targetParams));
29
+ await prepareConanDependencies(getBuildTargets(targetParams));
23
30
  await buildMissingCargoDependencies(getBuildTargets(targetParams), targetParams);
24
31
 
25
32
  const rebuildDeps = getRebuildDeps(state.config.allDependencies, rebuildOption);
@@ -54,6 +61,11 @@ export default async function buildDependencies({ targetParams, rebuildOption })
54
61
  }
55
62
  }
56
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
+
57
69
  // A cargo dependency carries every target in ONE package, so unlike a platform-split port there
58
70
  // is no "this sibling isn't for this target" case: a missing prebuilt means it was never built.
59
71
  // Nothing else builds these packages - buildDependencies only rebuilds deps that carry a source
@@ -61,40 +73,56 @@ export default async function buildDependencies({ targetParams, rebuildOption })
61
73
  // pre-build step nobody had written down. Worse, only the cmake link asserts the dependency is
62
74
  // there: on wasm a missing one silently drops out and the module builds clean, then dies at init.
63
75
  // Build it here instead, in the dependency's own package, where its prebuilt belongs.
64
- async function buildMissingCargoDependencies(targets, targetParams) {
65
- if (targets.length === 0) return;
76
+ async function buildMissingCargoDependencies(buildTargets, targetParams) {
77
+ if (buildTargets.length === 0) return;
66
78
  const appConfig = state.config;
67
- const missing = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig
68
- && dep.export?.type === 'cargo'
69
- && targets.some((target) => !dep.functions.isEnabled(target)));
79
+ const cargoDependencies = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig && dep.export?.type === 'cargo');
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;
86
+ // A prebuilt from another embind-rs counts as missing: linked, it carries stale glue or clashes with the others.
87
+ const fingerprint = getEmbindRsFingerprint();
88
+ const prebuiltOf = (dep, target) => [target.path, target.releasePath]
89
+ .map((targetPath) => `${dep.paths.output}/prebuilt/${targetPath}`)
90
+ .find((dir) => fs.existsSync(dir)) ?? `${dep.paths.output}/prebuilt/${target.path}`;
91
+ const isBuilt = (dep, target) => dep.functions.isEnabled(target) && !isEmbindRsFingerprintStale(prebuiltOf(dep, target), fingerprint);
92
+ const missing = cargoDependencies.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
70
93
 
71
94
  for (const dep of missing) {
72
95
  const name = dep.general.name;
73
96
  // isEnabled reads the filesystem on every call, so a build that lands while this one waits
74
97
  // for the lock is picked up without any state to refresh.
75
98
  await withDirLock(`${dep.paths.output}.autobuild.lock`, async () => {
76
- if (targets.every((target) => dep.functions.isEnabled(target))) return;
77
- logger.info(`crossbind: building cargo dependency "${name}" - it has no prebuilt for this target yet…`);
99
+ if (targets.every((target) => isBuilt(dep, target))) return;
100
+ logger.info(`crossbind: building cargo dependency "${name}" - it has no prebuilt from this embind-rs for this target yet…`);
78
101
  const scoped = await loadConfig(dep.paths.project);
79
102
  const prev = state.config;
80
103
  state.config = scoped;
81
104
  try {
82
105
  buildLib(targetParams);
106
+ if (extraTarget) buildLib(paramsOf(extraTarget));
83
107
  } finally {
84
108
  state.config = prev;
85
109
  }
86
110
  });
111
+ // loadConfig resolved a never-built package's CMakeLists to the CLI's own, and isEnabled and the link read it.
112
+ dep.paths.cmake = getAbsolutePath(dep.paths.project, getCMakeListsFilePath(dep.paths.output));
113
+ dep.paths.cmakeDir = getParentPath(dep.paths.cmake);
87
114
  }
115
+ if (missing.length > 0) appConfig.dependencyParameters = calculateDependencyParameters(appConfig);
88
116
 
89
117
  // The build above is the fix; this is the guard behind it. assertDependsBuilt only runs while
90
118
  // the cmake link is being configured, so a cached native build skips the check and a missing
91
119
  // cargo dependency reaches the app anyway - measured on wasm as a clean BUILD=0 whose module
92
120
  // then failed 22 of 43 conformance features at init. Here the answer is known either way.
93
- const unresolved = missing.filter((dep) => targets.some((target) => !dep.functions.isEnabled(target)));
121
+ const unresolved = missing.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
94
122
  if (unresolved.length > 0) {
95
123
  const names = unresolved.map((dep) => `"${dep.general.name}"`).join(', ');
96
124
  throw new Error(`crossbind: cargo ${unresolved.length > 1 ? 'dependencies' : 'dependency'} ${names} still `
97
- + `${unresolved.length > 1 ? 'have' : 'has'} no prebuilt for ${targets.map((t) => t.path).join(', ')} `
125
+ + `${unresolved.length > 1 ? 'have' : 'has'} no prebuilt built from the current embind-rs for ${targets.map((t) => t.path).join(', ')} `
98
126
  + 'after building. Linking without it produces a module that builds clean and dies at init.');
99
127
  }
100
128
  }
@@ -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
  });
@@ -13,7 +13,7 @@ const SUPPRESSED_WARN_CODES = new Set([
13
13
  'MIXED_EXPORTS',
14
14
  ]);
15
15
 
16
- function suppressNodeBuiltinWarnings(warning, defaultHandler) {
16
+ export function suppressNodeBuiltinWarnings(warning, defaultHandler) {
17
17
  if (SUPPRESSED_WARN_CODES.has(warning.code)) return;
18
18
  if (warning.code === 'UNRESOLVED_IMPORT' && /^node:/.test(warning.exporter || warning.source || warning.id || '')) return;
19
19
  defaultHandler(warning);
@@ -7,6 +7,10 @@ import state from '../state/index.js';
7
7
  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
+ 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';
10
14
 
11
15
  export default function buildLib(targetParams, options = {}) {
12
16
  let isChanged = false;
@@ -21,9 +25,10 @@ export default function buildLib(targetParams, options = {}) {
21
25
  // existence-only skip below would keep serving a stale staged staticlib after source edits.
22
26
  const isCargo = state.config.export?.type === 'cargo';
23
27
  const libdir = `${state.config.paths.output}/prebuilt/${target.path}`;
24
- const sourceFingerprint = getSourceFingerprint(state.config);
28
+ const sourceFingerprint = getSourceFingerprint(state.config, target);
25
29
  const sourceChanged = isSourceFingerprintStale(libdir, sourceFingerprint);
26
- if (isCargo || sourceChanged || !fs.existsSync(`${libdir}/lib`)) {
30
+ const nativeChanged = isNativeSourceNewerThan(`${libdir}/lib`);
31
+ if (isCargo || sourceChanged || nativeChanged || !fs.existsSync(`${libdir}/lib`)) {
27
32
  if (sourceChanged) {
28
33
  for (const stale of staleTargetDirectories({
29
34
  buildPath: state.config.paths.build,
@@ -32,8 +37,9 @@ export default function buildLib(targetParams, options = {}) {
32
37
  })) fs.rmSync(stale, { recursive: true, force: true });
33
38
  }
34
39
  // createLib caches on existence too, so re-entering is not enough: a stale stamp means
35
- // the lib on disk came from a different upstream release and has to be rebuilt.
36
- 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 });
37
43
 
38
44
  const modules = [];
39
45
  state.config.paths.module.forEach((modulePath) => {
@@ -48,12 +54,9 @@ export default function buildLib(targetParams, options = {}) {
48
54
  fs.copyFileSync(modulePath, `${state.config.paths.output}/prebuilt/${target.path}/swig/${fileName}`);
49
55
  });
50
56
  writeSourceFingerprint(libdir, sourceFingerprint);
57
+ if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint());
51
58
  isChanged = true;
52
59
  } else {
53
- // The skip is existence-only; without this warning a source edit is served stale silently.
54
- if (isNativeSourceNewerThan(`${state.config.paths.output}/prebuilt/${target.path}/lib`)) {
55
- 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`);
56
- }
57
60
  logger.cachedStep(target, 'lib');
58
61
  }
59
62
  });
@@ -64,6 +67,11 @@ export default function buildLib(targetParams, options = {}) {
64
67
  if (isChanged && fs.existsSync(`${state.config.paths.build}/Source-Debug/prebuilt`)) {
65
68
  fs.cpSync(`${state.config.paths.build}/Source-Debug/prebuilt`, `${state.config.paths.output}/prebuilt`, { recursive: true, dereference: true });
66
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);
67
75
 
68
76
  if (!options.skipXcframework) {
69
77
  createXCFramework();
@@ -127,3 +135,14 @@ export default function buildLib(targetParams, options = {}) {
127
135
  fs.writeFileSync(`${state.config.paths.output}/prebuilt/CMakeLists.txt`, distCmakeContent);
128
136
  }
129
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
+ }
@@ -0,0 +1,190 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { rollup } from 'rollup';
5
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
6
+ import commonjs from '@rollup/plugin-commonjs';
7
+ import virtual from '@rollup/plugin-virtual';
8
+ import run from './run.js';
9
+ import getLinkInputs from './getLinkInputs.js';
10
+ import getData from './getData.js';
11
+ import { suppressNodeBuiltinWarnings } from './buildJs.js';
12
+ import state from '../state/index.js';
13
+ import logger from '../utils/logger.js';
14
+ import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
15
+ import { buildLinkLibArgs } from '../utils/linkLayout.js';
16
+ import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
17
+ import scopedEmbind from '../utils/scopedEmbind.js';
18
+ import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
19
+ import { rustStdLibsFor } from '../utils/cargoTarget.js';
20
+ import toolchainNoticesDir from '../utils/toolchainNotices.js';
21
+
22
+ const cpuCount = Math.max(1, os.cpus().length - 1);
23
+ const WINDOWS_IMAGE_NOTICES = '/opt/licenses/llvm-mingw';
24
+
25
+ function filesUnder(dirs) {
26
+ return dirs.flatMap((dir) => (fs.existsSync(dir)
27
+ ? fs.readdirSync(dir, { recursive: true })
28
+ .map((file) => `${dir}/${file}`)
29
+ .filter((file) => fs.statSync(file).isFile())
30
+ : []));
31
+ }
32
+
33
+ function linkInputs(target) {
34
+ const {
35
+ libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
36
+ } = getLinkInputs(target, {
37
+ // The linker loads the archive member that defines a -u symbol; Mach-O prefixes C names with _.
38
+ keepFlag: (name) => `-Wl,-u,${target.platform === 'darwin' ? '_' : ''}crossbind_keep_${name}`,
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))];
43
+ const linkArgs = [
44
+ ...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad: target.platform === 'darwin' }),
45
+ ...rustKeepFlags,
46
+ // The system libraries the archives need, declared by the packages that bring them in.
47
+ ...(getData('binary', target)?.addonFlags ?? []),
48
+ ...(hasRust ? rustStdLibsFor(target) : []),
49
+ ];
50
+ // The addon runs embind-jsi, so Rust packages bind through the adapter React Native links.
51
+ const extraSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/jsi.cpp`] : [];
52
+ return { libs, linkArgs, extraSources };
53
+ }
54
+
55
+ function envOf(target) {
56
+ return Object.fromEntries(Object.entries(getData('env', target))
57
+ .map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
58
+ }
59
+
60
+ // 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
+
77
+ // One loader serves every addon of a build type, so each addon's env is keyed the way the loader
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.
80
+ export function nodeLoaderConfig(target) {
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;
84
+ return {
85
+ env: Object.fromEntries(loaded.map((t) => [`${t.platform}-${t.arch}`, envOf(t)])),
86
+ general: { name: state.config.general.name },
87
+ paths: { addon: target.addonPattern, ...(addonPackage ? { addonPackage } : {}) },
88
+ };
89
+ }
90
+
91
+ // The loader hands the addon dist/data by default, as the wasm build does. A relinked addon may come
92
+ // from updated dependencies, so its data is copied again rather than kept.
93
+ export function publishNodeData(target, { refresh }) {
94
+ Object.entries(getData('data', target)).forEach(([source, name]) => {
95
+ const published = `${state.config.paths.output}/data/${name}`;
96
+ if (!fs.existsSync(source) || (!refresh && fs.existsSync(published))) {
97
+ return;
98
+ }
99
+ fs.rmSync(published, { recursive: true, force: true });
100
+ fs.cpSync(source, published, { recursive: true });
101
+ });
102
+ }
103
+
104
+ async function bundleLoader(target, napiRoot, loaderConfig) {
105
+ const systemConfig = `export default ${JSON.stringify(loaderConfig)};`;
106
+ const bundle = await rollup({
107
+ input: `${napiRoot}/js/loader.js`,
108
+ plugins: [
109
+ virtual({ 'crossbind/systemConfig': systemConfig }), scopedEmbind(`${resolveEmbindJsiRoot()}/js/embind.js`), nodeResolve(), commonjs(),
110
+ ],
111
+ onwarn: suppressNodeBuiltinWarnings,
112
+ });
113
+ await bundle.write({ file: `${state.config.paths.build}/${target.jsName}`, format: 'cjs', exports: 'default' });
114
+ await bundle.close();
115
+ }
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
+
122
+ // Links one Node-API addon from the project's archives and bundles the loader that picks the
123
+ // addon matching the running process.
124
+ export default async function buildNode(target, options = {}) {
125
+ if (target.platform === 'darwin' && process.platform !== 'darwin') {
126
+ logger.info(`[${target.path}] addon skipped (macOS addons build on a macOS host)`);
127
+ return false;
128
+ }
129
+ if (state.config.export.type === 'cargo') {
130
+ logger.info(`[${target.path}] addon skipped (cargo package - staticlib only)`);
131
+ return false;
132
+ }
133
+
134
+ const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
135
+ const { build } = state.config.paths;
136
+ const napiRoot = resolveEmbindNapiRoot();
137
+ const jsiRoot = resolveEmbindJsiRoot();
138
+ const { libs, linkArgs, extraSources } = linkInputs(target);
139
+ const loaderConfig = nodeLoaderConfig(target);
140
+
141
+ const fingerprintFile = `${build}/${target.addonName}.fingerprint`;
142
+ const fingerprint = getContentHash(JSON.stringify({
143
+ builder: getFilesFingerprint([fileURLToPath(import.meta.url)]),
144
+ linkArgs,
145
+ libs: libs.map((lib) => {
146
+ const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
147
+ return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
148
+ }),
149
+ loaderConfig,
150
+ runtime: getFilesFingerprint(filesUnder([
151
+ `${napiRoot}/cpp`, `${napiRoot}/js`, `${napiRoot}/third_party`, `${jsiRoot}/cpp/src`, `${jsiRoot}/js`,
152
+ ])),
153
+ extraSources: getFilesFingerprint(extraSources),
154
+ }));
155
+ const isLinkChanged = !fs.existsSync(fingerprintFile)
156
+ || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint;
157
+ if (!options.force && !isLinkChanged
158
+ && fs.existsSync(`${build}/${target.addonName}`) && fs.existsSync(`${build}/${target.jsName}`)) {
159
+ logger.cachedStep(target, 'addon');
160
+ return false;
161
+ }
162
+
163
+ const platformPrefix = `Node-${buildType}`;
164
+ logger.startStep(target, 'addon');
165
+ run(null, [
166
+ 'cmake', `${napiRoot}/cpp`,
167
+ `-DCMAKE_BUILD_TYPE=${buildType}`,
168
+ `-DCROSSBIND_ADDON_FILE=${target.addonName}`,
169
+ `-DCROSSBIND_JSI_ROOT=${jsiRoot}`,
170
+ `-DCROSSBIND_EXTRA_SOURCES=${extraSources.join(';')}`,
171
+ `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
172
+ `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
173
+ ], 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
+ }
181
+ fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
182
+ logger.doneStep(target, 'addon');
183
+
184
+ logger.startStep(target, 'js');
185
+ await bundleLoader(target, napiRoot, loaderConfig);
186
+ logger.doneStep(target, 'js');
187
+
188
+ fs.writeFileSync(fingerprintFile, fingerprint);
189
+ return true;
190
+ }
@@ -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;