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
@@ -12,6 +12,9 @@ import logger from '../utils/logger.js';
12
12
  import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
13
13
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
14
14
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
15
+ import { targetArchiveFlags } from '../utils/archiveFlags.js';
16
+ import { conanInputsOf } from '../utils/conanDependencies.js';
17
+ import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
15
18
 
16
19
  const cpuCount = Math.max(1, os.cpus().length - 1);
17
20
  const sharedPlatforms = ['android'];
@@ -21,7 +24,7 @@ export default function createLib(target, fileType, options = {}) {
21
24
  throw new Error('invalid target or options');
22
25
  }
23
26
 
24
- if (target.platform === 'ios' && process.platform !== 'darwin') {
27
+ if ((target.platform === 'ios' || target.platform === 'darwin') && process.platform !== 'darwin') {
25
28
  return;
26
29
  }
27
30
 
@@ -29,20 +32,31 @@ export default function createLib(target, fileType, options = {}) {
29
32
  const platformPrefix = `${fileType ? `${fileType}-` : ''}${buildType}`;
30
33
  const libdir = `${state.config.paths.build}/${platformPrefix}/prebuilt/${target.path}`;
31
34
  const buildPath = `${state.config.paths.build}/${platformPrefix}/${target.path}`;
35
+ const sourceDirs = options.buildSource ? state.config.paths.native : [];
36
+ const sourcesChanged = nativeSourceStampChanged(`${libdir}/lib`, sourceDirs);
37
+ // Automatically loaded cargo packages have no CMake dependency paths.
38
+ const depPaths = state.config.allDependencyPaths?.[target.path];
39
+ const sourceReplacements = state.config.build?.sourceReplaceList?.(target, depPaths) ?? [];
32
40
  // wasi configure tool links need the runtime stubs; compiled below, referenced from LIBS.
33
41
  const wasiStubObj = `${buildPath}/crossbind-wasi-stubs.o`;
34
42
 
35
43
  const nativeGlob = options.nativeGlob ? withDependencyBridges(options.nativeGlob) : undefined;
36
44
  // The lib dir is reused while the bridge list grows, so an early smaller nativeGlob build must not satisfy later sets: fingerprint the glob and miss on mismatch.
37
45
  const fingerprintFile = `${libdir}/crossbind-nativeglob.fingerprint`;
38
- const fingerprint = nativeGlob ? getFilesFingerprint(nativeGlob) : null;
46
+ // options.inputs lists what the sources compile against (a binding runtime's headers), so a change there rebuilds too.
47
+ const fingerprint = nativeGlob ? getFilesFingerprint([...nativeGlob, ...(options.inputs ?? [])]) : null;
39
48
  const fingerprintChanged = fingerprint !== null
40
49
  && (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
41
50
 
42
- // Config emccFlags feed compile-time state too (CROSSBIND_JSPI below), so a flag change must miss this cache.
51
+ // Config emccFlags feed compile-time state too (CROSSBIND_JSPI below), so a flag change must miss this cache, and so
52
+ // does a compile option a dependency declares (Lerc's LERC_STATIC on Windows). Without one the hash stays as it was.
43
53
  const configEmccFlags = getData('binary', target)?.emccFlags || [];
54
+ const compileOptions = getData('cmake', target)?.compileOptions || [];
44
55
  const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
45
- const flagsFingerprint = getContentHash(JSON.stringify(configEmccFlags));
56
+ const conanInputs = conanInputsOf(state.config);
57
+ const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
58
+ const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
59
+ const flagsFingerprint = getContentHash(JSON.stringify(conanInputs ? [flagsInputs, conanInputs] : flagsInputs));
46
60
  const flagsChanged = !fs.existsSync(flagsFingerprintFile)
47
61
  || fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
48
62
 
@@ -64,7 +78,7 @@ export default function createLib(target, fileType, options = {}) {
64
78
  return changed;
65
79
  }
66
80
 
67
- if (!options.force && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
81
+ if (!options.force && !sourcesChanged && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
68
82
  logger.cachedStep(target, fileType);
69
83
  return false;
70
84
  }
@@ -78,7 +92,6 @@ export default function createLib(target, fileType, options = {}) {
78
92
 
79
93
  const buildEnv = { params: [] };
80
94
  let buildParams;
81
- const depPaths = state.config.allDependencyPaths[target.path];
82
95
  if (state.config.build.withBuildConfig) {
83
96
  const { getBuildParams, getExtraLibs } = state.config.build;
84
97
  buildEnv.console = true;
@@ -105,31 +118,20 @@ export default function createLib(target, fileType, options = {}) {
105
118
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
106
119
 
107
120
  triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
108
- if (target.runtime === 'mt') {
109
- cFlags.push('-pthread');
110
- ldFlags.push('-pthread');
111
- }
112
-
113
- if (target.platform === 'wasm') {
114
- cFlags.push('-msimd128');
115
- ldFlags.push('-msimd128');
116
- }
121
+ const archiveFlags = targetArchiveFlags(target);
122
+ cFlags.push(...archiveFlags);
123
+ ldFlags.push(...archiveFlags);
117
124
 
118
125
  if (isJspiTarget) {
119
126
  cFlags.push('-DCROSSBIND_JSPI');
120
127
  }
121
128
 
122
- if (target.platform === 'wasm' && target.arch === 'wasm64') {
123
- cFlags.push('-sMEMORY64=1');
124
- ldFlags.push('-sMEMORY64=1');
125
- }
126
-
127
129
  buildEnv.params.push('-e', `CFLAGS=${cFlags.join(' ')}`);
128
130
  buildEnv.params.push('-e', `CXXFLAGS=${cFlags.join(' ')}`);
129
131
  buildEnv.params.push('-e', `LDFLAGS=${ldFlags.join(' ')} ${extraLibs.join(' ')}`);
130
132
  // Emulation archives must land in LIBS (after the objects), not LDFLAGS.
131
133
  const wasiStubLib = state.config.build?.buildType === 'configure' ? `${wasiStubObj} ` : '';
132
- const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')}` : '';
134
+ const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')} -Wl,--wrap=lseek` : '';
133
135
  buildEnv.params.push('-e', `LIBS=${dependLibs} ${extraLibs.join(' ')}${wasiLibs}`);
134
136
 
135
137
  let configBuildEnv = state.config.build.env;
@@ -144,22 +146,11 @@ export default function createLib(target, fileType, options = {}) {
144
146
 
145
147
  triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
146
148
 
147
- if (target.runtime === 'mt') {
148
- buildEnv.params.push('-e', `CFLAGS=-pthread`);
149
- buildEnv.params.push('-e', `CXXFLAGS=-pthread`);
150
- buildEnv.params.push('-e', `LDFLAGS=-pthread`);
151
- }
152
-
153
- if (target.platform === 'wasm') {
154
- buildEnv.params.push('-e', `CFLAGS=-msimd128`);
155
- buildEnv.params.push('-e', `CXXFLAGS=-msimd128`);
156
- buildEnv.params.push('-e', `LDFLAGS=-msimd128`);
157
- }
158
-
159
- if (target.platform === 'wasm' && target.arch === 'wasm64') {
160
- buildEnv.params.push('-e', `CFLAGS=-sMEMORY64=1`);
161
- buildEnv.params.push('-e', `CXXFLAGS=-sMEMORY64=1`);
162
- buildEnv.params.push('-e', `LDFLAGS=-sMEMORY64=1`);
149
+ const archiveFlags = targetArchiveFlags(target).join(' ');
150
+ if (archiveFlags) {
151
+ buildEnv.params.push('-e', `CFLAGS=${archiveFlags}`);
152
+ buildEnv.params.push('-e', `CXXFLAGS=${archiveFlags}`);
153
+ buildEnv.params.push('-e', `LDFLAGS=${archiveFlags}`);
163
154
  }
164
155
 
165
156
  if (isJspiTarget) {
@@ -170,7 +161,17 @@ export default function createLib(target, fileType, options = {}) {
170
161
 
171
162
  logger.startStep(target, fileType);
172
163
  const t0 = performance.now();
173
- const cmakeDir = state.config.build.withBuildConfig ? `${state.config.paths.build}/source` : state.config.paths.cmakeDir;
164
+ let cmakeDir = state.config.build.withBuildConfig ? `${state.config.paths.build}/source` : state.config.paths.cmakeDir;
165
+ if (state.config.build?.buildType !== 'configure' && sourceReplacements.length) {
166
+ // Patches belong to a target's copy: a platform-specific change must not alter another target's inputs.
167
+ const patchedSource = `${buildPath}/crossbind-source`;
168
+ fs.mkdirSync(buildPath, { recursive: true });
169
+ fs.cpSync(cmakeDir, patchedSource, { recursive: true, preserveTimestamps: true });
170
+ sourceReplacements.forEach(({ regex, replacement, paths }) => {
171
+ replace({ regex, replacement, paths: paths.map((p) => `${patchedSource}/${p}`), recursive: false, silent: true });
172
+ });
173
+ cmakeDir = patchedSource;
174
+ }
174
175
 
175
176
  if (state.config.build?.beforeRun) {
176
177
  const dataList = state.config.build?.beforeRun(cmakeDir);
@@ -190,11 +191,24 @@ export default function createLib(target, fileType, options = {}) {
190
191
  ], platformPrefix, target, { console: buildEnv.console });
191
192
  };
192
193
 
193
- if (!options.bypassCmake) {
194
+ // A configure build's make install runs coreutils install, whose chmod on a file it has just written
195
+ // Docker Desktop's file sharing now and then refuses; the wrapper finishes it by path. Staged like stubs.c.
196
+ const stageInstall = () => {
197
+ const staged = `${buildPath}/crossbind-install.sh`;
198
+ fs.mkdirSync(buildPath, { recursive: true });
199
+ fs.copyFileSync(`${state.config.paths.cli}/assets/configure/install.sh`, staged);
200
+ fs.chmodSync(staged, 0o755);
201
+ return staged;
202
+ };
203
+
204
+ if (!options.bypassCmake || sourcesChanged) {
194
205
  if (state.config.build?.buildType === 'configure') {
195
- fs.cpSync(cmakeDir, buildPath, { recursive: true });
196
- if (state.config.build?.sourceReplaceList) {
197
- state.config.build.sourceReplaceList(target, depPaths)?.forEach(({ regex, replacement, paths }) => {
206
+ // make compares times, so the tree starts afresh with the extracted times: no source then looks newer
207
+ // than what it generates, and no object of an earlier build looks newer than its source.
208
+ fs.rmSync(buildPath, { recursive: true, force: true });
209
+ fs.cpSync(cmakeDir, buildPath, { recursive: true, preserveTimestamps: true });
210
+ if (sourceReplacements.length) {
211
+ sourceReplacements.forEach(({ regex, replacement, paths }) => {
198
212
  replace({
199
213
  regex, replacement, paths: paths.map((p) => `${buildPath}/${p}`), recursive: false, silent: true,
200
214
  });
@@ -231,8 +245,10 @@ export default function createLib(target, fileType, options = {}) {
231
245
  // Some upstream makefiles race when one -j invocation carries multiple goals (openssl's
232
246
  // install builds apps twice); recipes can split phases via build.makePhases.
233
247
  const makePhases = state.config.build?.makePhases || [['install']];
248
+ // A make command-line variable beats the Makefile's own INSTALL, autosetup's (sqlite3) included.
249
+ const installParams = state.config.build?.buildType === 'configure' ? [`INSTALL=${stageInstall()}`] : [];
234
250
  makePhases.forEach((phase) => {
235
- run(null, ['make', `-j${cpuCount}`, ...phase], platformPrefix, target, { console: buildEnv.console });
251
+ run(null, ['make', `-j${cpuCount}`, ...installParams, ...phase], platformPrefix, target, { console: buildEnv.console, exclusive: true });
236
252
  });
237
253
  }
238
254
  const t2 = performance.now();
@@ -246,6 +262,7 @@ export default function createLib(target, fileType, options = {}) {
246
262
  fs.writeFileSync(fingerprintFile, fingerprint);
247
263
  }
248
264
  fs.writeFileSync(flagsFingerprintFile, flagsFingerprint);
265
+ writeNativeSourceStamp(`${libdir}/lib`, sourceDirs);
249
266
  // Callers can force the final link when a lib actually rebuilt.
250
267
  return true;
251
268
  }
@@ -13,7 +13,7 @@ export default function getAllBridges(log = console.log) {
13
13
  if (!fs.existsSync(sidecar)) return true;
14
14
  const source = fs.readFileSync(sidecar, 'utf8').trim();
15
15
  if (!source || fs.existsSync(source)) return true;
16
- [bridge, `${bridge}.exports.json`, `${bridge}.deps`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
16
+ [bridge, `${bridge}.exports.json`, `${bridge}.deps`, `${bridge}.warnings`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
17
17
  log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - its header is gone`);
18
18
  return false;
19
19
  });
@@ -3,7 +3,10 @@ import state from '../state/index.js';
3
3
  import { getFilteredTargetSpec, getBuildTargets } from './target.js';
4
4
 
5
5
  function getRecursiveData(obj, config, dependency, field, target) {
6
- const entryArray = getFilteredTargetSpec(dependency?.targetSpecs, target).map(s => s[field]);
6
+ // An app may list a port's wasm, Android and iOS packages together. One with no prebuilt for the target
7
+ // drops out of the link, and its data stays out with it.
8
+ const skipsTarget = field === 'data' && dependency !== config && dependency.functions?.isEnabled && !dependency.functions.isEnabled(target);
9
+ const entryArray = skipsTarget ? [] : getFilteredTargetSpec(dependency?.targetSpecs, target).map(s => s[field]);
7
10
  const entries = Object.assign({}, ...entryArray);
8
11
  Object.entries(entries).forEach(([dKey, value]) => {
9
12
  if (field === 'data') {
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import getDependLibs from './getDependLibs.js';
3
+ import state from '../state/index.js';
4
+ import buildAppRustCrates from '../utils/appRustCrates.js';
5
+
6
+ // The archives of a final link (wasm, Node-API addon or native executable) and which of them must
7
+ // survive whole. keepFlag spells "keep this symbol" for the target's linker; a native executable
8
+ // binds nothing, so it links no bridge.
9
+ export default function getLinkInputs(target, { keepFlag, withBridge = true }) {
10
+ const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
11
+ const { build, output, cache } = state.config.paths;
12
+ const { name } = state.config.general;
13
+
14
+ // buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
15
+ // build-dir copy can be gone while the output artifact is still valid — link it then.
16
+ const sourceLibCandidates = [
17
+ `${build}/Source-${buildType}/${target.path}/lib${name}.a`,
18
+ `${output}/prebuilt/${target.path}/lib/lib${name}.a`,
19
+ ];
20
+ // App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
21
+ // single fully-loaded Rust archive of the link (see the libstd rule below).
22
+ const appRustLibs = buildAppRustCrates(target, cache, state.config.cargoDependencies ?? {});
23
+ const libs = [
24
+ ...getDependLibs(target),
25
+ ...appRustLibs,
26
+ sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
27
+ ...(withBridge ? [`${build}/Bridge-${buildType}/${target.path}/lib${name}.a`] : []),
28
+ ];
29
+
30
+ // By default only the Bridge archive is kept whole (see linkLayout.js) so the linker
31
+ // dead-code-eliminates everything unreferenced. Two escape hatches, both
32
+ // `export.wholeArchive: true`: on the APP it restores the legacy layout (every archive
33
+ // wholesale); on a LIBRARY's own config it keeps that library's archives whole in every
34
+ // consumer link (for members that self-register from static initializers).
35
+ const wholeArchiveAll = state.config.export.wholeArchive === true;
36
+ const wholeArchiveNames = new Set();
37
+ // Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
38
+ // may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
39
+ // with their keep symbol pinned (keepFlag pulls just the registration object); manual-bindings
40
+ // crates have no keep symbol and fall back to whole-archive - safe only while they are the
41
+ // single loaded Rust archive.
42
+ // A COFF link cannot load the super whole either: two crates in it may import one DLL through raw-dylib (std
43
+ // and getrandom both take ProcessPrng from bcryptprimitives.dll), and lld fails on the repeated import tables.
44
+ // There it links like a package bridge, through its keep symbol.
45
+ const rustKeepFlags = [];
46
+ if (appRustLibs.length > 0) {
47
+ if (target.platform === 'win32') rustKeepFlags.push(keepFlag('crossbind_app_super'));
48
+ else wholeArchiveNames.add('crossbind_app_super');
49
+ }
50
+ const depends = state.config.dependencyParameters.getCmakeDepends(target);
51
+ depends.forEach((dep) => {
52
+ if (dep.export.wholeArchive === true) {
53
+ (dep.export.libName || []).forEach((libName) => wholeArchiveNames.add(libName));
54
+ }
55
+ if (dep.export.type === 'cargo') {
56
+ const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
57
+ const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
58
+ (dep.export.libName || []).forEach((libName) => {
59
+ if (isManual) wholeArchiveNames.add(libName);
60
+ else rustKeepFlags.push(keepFlag(libName));
61
+ });
62
+ }
63
+ });
64
+
65
+ const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
66
+ || depends.some((dep) => dep.export.type === 'cargo');
67
+ return {
68
+ libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
69
+ };
70
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import p from 'node:path';
3
3
  import state from '../state/index.js';
4
+ import { nativeSourceStampChanged } from '../utils/nativeSourceStamp.js';
4
5
 
5
6
  function newestMtimeInDir(dir) {
6
7
  let newest = 0;
@@ -22,6 +23,7 @@ function newestMtimeInDir(dir) {
22
23
  // (bundler plugins: the built js) or a directory (CLI lib cache: the staged prebuilt dir).
23
24
  export function isNativeSourceNewerThan(artifactPath) {
24
25
  if (!fs.existsSync(artifactPath)) return false;
26
+ if (nativeSourceStampChanged(artifactPath, state.config.paths.native)) return true;
25
27
  const stat = fs.statSync(artifactPath);
26
28
  const artifactMtime = stat.isDirectory() ? newestMtimeInDir(artifactPath) : stat.mtimeMs;
27
29
  for (const dir of state.config.paths.native) {
@@ -1,14 +1,24 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  import state from '../state/index.js';
4
5
  import loadJs from '../utils/loadJs.js';
5
6
  import { isCopyleft } from '../utils/licenseReport.js';
6
7
  import familyManifestOf from '../utils/familyManifest.js';
7
8
  import { wasiToolchainIdentity } from '../utils/provenance.js';
8
9
  import { resolveWasiSdkPath } from '../utils/wasiToolchain.js';
10
+ import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
11
+ import { shippedBundledLicense } from '../utils/bundledLicenses.js';
12
+ import toolchainNoticesDir from '../utils/toolchainNotices.js';
9
13
 
10
14
  const LEGACY_LICENSE_FILES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'];
11
15
  const WASI_LIBC_LICENSE = 'Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT';
16
+ const CROSSBIND_ROOT = fileURLToPath(new URL('../..', import.meta.url));
17
+ const MISSING_TEXT = '(missing: build the package once to extract the upstream source)';
18
+
19
+ function packageLicenseFile(dir) {
20
+ return LEGACY_LICENSE_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file));
21
+ }
12
22
 
13
23
  // License texts come from any extracted upstream source tree of the family (variants share the tarball).
14
24
  function findSourceDir(familyDir) {
@@ -20,20 +30,21 @@ function findSourceDir(familyDir) {
20
30
  return null;
21
31
  }
22
32
 
23
- function readLicenseTexts(sourceDir, files) {
33
+ function readLicenseTexts(files, locate) {
24
34
  const texts = (files || []).map((file) => {
25
- const filePath = sourceDir ? path.join(sourceDir, file) : null;
35
+ const filePath = locate(file);
26
36
  const body = filePath && fs.existsSync(filePath)
27
37
  ? fs.readFileSync(filePath, 'utf8').trim()
28
- : '(missing: build the package once to extract the upstream source)';
38
+ : MISSING_TEXT;
29
39
  return `=== ${file} ===\n\n${body}`;
30
40
  });
31
41
  return texts.length > 0 ? texts.join('\n\n') : null;
32
42
  }
33
43
 
34
44
  // Vendored copies compiled into the artifact (recipe `bundled` map, keyed by platform)
35
- // become first-class notice/SBOM rows with texts from the family source tree.
36
- async function bundledRowsOf(node, platform) {
45
+ // become first-class notice/SBOM rows with texts from the family source tree, or from the dist
46
+ // of whichever package of the family shipped them.
47
+ async function bundledRowsOf(node, platform, familyProjects) {
37
48
  const recipe = await loadJs(node.paths.project, 'crossbind.build');
38
49
  const entries = recipe?.bundled?.[platform];
39
50
  if (!entries || entries.length === 0) return [];
@@ -50,7 +61,9 @@ async function bundledRowsOf(node, platform) {
50
61
  licenseNotes: `${entry.notes ? `${entry.notes}; ` : ''}vendored inside ${node.general.name} ${node.package?.nativeVersion || ''}`.trim(),
51
62
  sha256: null,
52
63
  sourceUrl: null,
53
- licenseText: readLicenseTexts(sourceDir, entry.files),
64
+ licenseText: readLicenseTexts(entry.files, (file) => (sourceDir
65
+ ? path.join(sourceDir, file)
66
+ : shippedBundledLicense(familyProjects, entry.name, file))),
54
67
  isCopyleft: isCopyleft(entry.license),
55
68
  }));
56
69
  }
@@ -90,6 +103,98 @@ function wasiToolchainRows() {
90
103
  ];
91
104
  }
92
105
 
106
+ // The notices a build copied out of its toolchain image (buildNode does it for Windows addons).
107
+ function toolchainNoticeTexts(platform) {
108
+ const build = state.config?.paths?.build;
109
+ const dir = build ? path.join(build, toolchainNoticesDir(platform)) : null;
110
+ if (!dir || !fs.existsSync(dir)) return null;
111
+ const texts = fs.readdirSync(dir).sort()
112
+ .map((file) => `=== ${file} ===\n\n${fs.readFileSync(path.join(dir, file), 'utf8').trim()}`);
113
+ return texts.length > 0 ? texts.join('\n\n') : null;
114
+ }
115
+
116
+ // A Linux or Windows addon carries its C++ runtime; a Windows one also carries parts of the
117
+ // mingw-w64 runtime and winpthreads, whose licenses ask for their notices in binary distributions.
118
+ // A macOS addon uses the system's C++ runtime.
119
+ function addonToolchainRows(platform) {
120
+ const shared = {
121
+ npmName: null, version: null, nativeVersion: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
122
+ };
123
+ const llvmLicense = 'Apache-2.0 WITH LLVM-exception';
124
+ const llvmRuntimes = {
125
+ ...shared,
126
+ name: 'llvm-runtimes',
127
+ license: llvmLicense,
128
+ licenseDeclared: llvmLicense,
129
+ sourceUrl: 'https://github.com/llvm/llvm-project',
130
+ };
131
+ const llvmText = 'license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT';
132
+ if (platform === 'linux' || platform === 'linuxmusl') {
133
+ return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the addon; ${llvmText}` }];
134
+ }
135
+ const mingwLicense = 'LicenseRef-MinGW-w64-runtime';
136
+ return [
137
+ { ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the addon; ${llvmText}` },
138
+ {
139
+ ...shared,
140
+ name: 'mingw-w64-runtime',
141
+ license: mingwLicense,
142
+ licenseDeclared: mingwLicense,
143
+ sourceUrl: 'https://github.com/mingw-w64/mingw-w64',
144
+ licenseNotes: 'the mingw-w64 startup code and winpthreads, statically linked into the addon; a binary distribution includes their notices: https://github.com/mingw-w64/mingw-w64/blob/master/COPYING.MinGW-w64-runtime/COPYING.MinGW-w64-runtime.txt and https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-libraries/winpthreads/COPYING (the windows image carries both in /opt/licenses/llvm-mingw)',
145
+ licenseText: toolchainNoticeTexts('win32'),
146
+ },
147
+ ];
148
+ }
149
+
150
+ // A Node-API addon compiles in crossbind's runtime: embind-jsi, which adapts Emscripten's embind,
151
+ // and node-api-jsi. The loader bundles the JavaScript half of the same runtime.
152
+ function nodeRuntimeRows({ isAddon }) {
153
+ const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
154
+ const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
155
+ const shared = {
156
+ nativeVersion: null, sha256: null, licenseSelected: null, isCopyleft: false,
157
+ };
158
+ const jsiRoot = resolveEmbindJsiRoot();
159
+ const rows = [
160
+ {
161
+ ...shared,
162
+ name: 'crossbind',
163
+ npmName: 'crossbind',
164
+ version: versionOf(CROSSBIND_ROOT),
165
+ license: 'MIT',
166
+ licenseDeclared: 'MIT',
167
+ sourceUrl: 'https://github.com/crossbind/crossbind',
168
+ licenseNotes: 'the embind-jsi and embind-napi runtime and the bindings crossbind generates',
169
+ licenseText: textOf(path.join(CROSSBIND_ROOT, 'LICENSE')),
170
+ },
171
+ {
172
+ ...shared,
173
+ name: 'emscripten-embind',
174
+ npmName: '@crossbind/core-embind-jsi',
175
+ version: versionOf(jsiRoot),
176
+ license: 'MIT OR NCSA',
177
+ licenseDeclared: 'MIT OR NCSA',
178
+ sourceUrl: 'https://github.com/emscripten-core/emscripten',
179
+ licenseNotes: "Emscripten's embind, adapted by @crossbind/core-embind-jsi",
180
+ licenseText: textOf(path.join(jsiRoot, 'LICENSE')),
181
+ },
182
+ ];
183
+ if (!isAddon) return rows;
184
+ const napiRoot = resolveEmbindNapiRoot();
185
+ return [...rows, {
186
+ ...shared,
187
+ name: 'node-api-jsi',
188
+ npmName: '@crossbind/core-embind-napi',
189
+ version: versionOf(napiRoot),
190
+ license: 'MIT',
191
+ licenseDeclared: 'MIT',
192
+ sourceUrl: 'https://github.com/microsoft/node-api-jsi',
193
+ licenseNotes: 'JSI over Node-API, with the local patches @crossbind/core-embind-napi lists',
194
+ licenseText: textOf(path.join(napiRoot, 'third_party', 'node-api-jsi', 'LICENSE')),
195
+ }];
196
+ }
197
+
93
198
  async function buildRow(node) {
94
199
  const recipe = await loadJs(node.paths.project, 'crossbind.build');
95
200
  const manifest = node.package || null;
@@ -110,12 +215,18 @@ async function buildRow(node) {
110
215
  let licenseText;
111
216
  if (upstream) {
112
217
  license = upstream.selected || upstream.declared;
113
- licenseText = readLicenseTexts(findSourceDir(family.dir), upstream.files);
218
+ const sourceDir = findSourceDir(family.dir);
219
+ const fromSource = sourceDir ? readLicenseTexts(upstream.files, (file) => path.join(sourceDir, file)) : null;
220
+ const isComplete = Boolean(fromSource) && !fromSource.includes(MISSING_TEXT);
221
+ // An installed package has no extracted source, and some upstreams name no license file; the package
222
+ // ships the upstream text as its LICENSE either way.
223
+ const shipped = isComplete ? null : packageLicenseFile(node.paths.project);
224
+ if (isComplete) licenseText = fromSource;
225
+ else if (shipped) licenseText = `=== ${path.basename(shipped)} ===\n\n${fs.readFileSync(shipped, 'utf8').trim()}`;
226
+ else licenseText = readLicenseTexts(upstream.files, () => null);
114
227
  } else {
115
228
  license = manifest?.license || null;
116
- const legacy = LEGACY_LICENSE_FILES
117
- .map((name) => `${node.paths.project}/${name}`)
118
- .find((file) => fs.existsSync(file));
229
+ const legacy = packageLicenseFile(node.paths.project);
119
230
  licenseText = legacy ? fs.readFileSync(legacy, 'utf8') : null;
120
231
  }
121
232
 
@@ -135,22 +246,69 @@ async function buildRow(node) {
135
246
  };
136
247
  }
137
248
 
249
+ // A Conan package's facts come from its recipe, and its texts from the licenses/ its package ships.
250
+ function conanRow(node) {
251
+ const { conan } = node.general;
252
+ const licenseDir = `${node.paths.project}/licenses`;
253
+ const files = fs.existsSync(licenseDir)
254
+ ? fs.readdirSync(licenseDir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile())
255
+ : [];
256
+ const texts = files.map((entry) => path.join(entry.parentPath, entry.name)).sort()
257
+ .map((file) => `=== ${path.relative(licenseDir, file)} ===\n\n${fs.readFileSync(file, 'utf8').trim()}`);
258
+ return {
259
+ name: conan.name,
260
+ npmName: null,
261
+ version: null,
262
+ nativeVersion: conan.version,
263
+ license: conan.license,
264
+ licenseDeclared: null,
265
+ licenseSelected: null,
266
+ licenseNotes: `built from the ConanCenter recipe ${conan.ref}`,
267
+ sha256: conan.source?.sha256 ?? null,
268
+ sourceUrl: conan.source?.url ?? conan.homepage ?? null,
269
+ licenseText: texts.length > 0 ? texts.join('\n\n') : null,
270
+ isCopyleft: isCopyleft(conan.license),
271
+ purl: `pkg:conan/${conan.name}@${conan.version}`,
272
+ };
273
+ }
274
+
275
+ // Conan packages join the graph from the stage a build makes; without it they would be left out unseen.
276
+ function assertConanStaged() {
277
+ const attached = new Set(state.config.allDependencies.filter((d) => d.general.conan).map((d) => d.general.conan.name));
278
+ const missing = Object.keys(state.config.conanDependencies ?? {}).filter((name) => !attached.has(name));
279
+ if (missing.length > 0) {
280
+ throw new Error(`crossbind: the Conan packages ${missing.join(', ')} are not installed for the current conanDependencies, so their rows would be missing. Build the project first.`);
281
+ }
282
+ }
283
+
138
284
  // platform (optional, e.g. 'wasi') additionally includes what that platform's
139
285
  // artifact statically links beyond the package graph: recipe-declared vendored
140
- // copies and the toolchain runtime.
141
- export default async function collectLicenseRows(platform = null) {
142
- // The root package is a component too: leaf -wasi packages have no deps but ship their own upstream.
286
+ // copies and the toolchain runtime. runtimeEnv 'node' adds the crossbind runtime an
287
+ // addon of that platform compiles in, or without a platform the one its loader bundles.
288
+ export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
289
+ assertConanStaged();
290
+ // The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
291
+ // their own upstream. A dependency that is no port, such as a library a project publishes, is one by
292
+ // its package name; a private package is the project's own code, like the app.
293
+ const keyOf = (node) => (node.general.conan ? `conan:${node.general.conan.name}` : node.general.alias?.package ?? node.package?.name);
143
294
  const nodes = [state.config, ...state.config.allDependencies]
144
- .filter((node) => node?.general?.alias?.package && node.paths?.project);
295
+ .filter((node, index) => node?.paths?.project
296
+ && (node.general?.alias?.package || node.general?.conan || (index > 0 && node.package?.name && !node.package.private)));
145
297
  const rows = [];
146
298
  const seen = new Set();
147
299
  for (const node of nodes) {
148
- const key = node.general.alias.package;
300
+ const key = keyOf(node);
149
301
  if (seen.has(key)) continue;
150
302
  seen.add(key);
151
- rows.push(await buildRow(node));
152
- if (platform) rows.push(...await bundledRowsOf(node, platform));
303
+ rows.push(node.general.conan ? conanRow(node) : await buildRow(node));
304
+ // A Conan package's folder is written from what its recipe produced: nothing there is run.
305
+ if (platform && !node.general.conan) {
306
+ const familyProjects = nodes.filter((other) => keyOf(other) === key).map((other) => other.paths.project);
307
+ rows.push(...await bundledRowsOf(node, platform, familyProjects));
308
+ }
153
309
  }
154
310
  if (platform === 'wasi') rows.push(...wasiToolchainRows());
311
+ if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
312
+ if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
155
313
  return rows.sort((a, b) => a.name.localeCompare(b.name));
156
314
  }
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs';
2
+ import state, { setAllDependecyPaths } from '../state/index.js';
3
+ import refreshConanDependencies from '../state/refreshConanDependencies.js';
4
+ import installConanPackages from '../utils/conanInstall.js';
5
+ import { releaseTargetOf, HOST_BUILT_PLATFORMS } from '../utils/targets.js';
6
+ import createXCFramework from './createXCFramework.js';
7
+
8
+ const mtimeOf = (file) => fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? -Infinity;
9
+ // iOS and macOS build on a Mac alone: elsewhere the build leaves them out, and so does the staging.
10
+ const isBuiltHere = (target) => process.platform === 'darwin' || !HOST_BUILT_PLATFORMS.includes(target.platform);
11
+
12
+ // iOS headers and archives come out of an xcframework per library with a slice per SDK (state/index.js).
13
+ // One older than an archive it wraps was made before the package was staged again.
14
+ function makeConanXCFrameworks(iosTargets) {
15
+ const archiveOf = (dependency, target, name) => `${dependency.paths.output}/prebuilt/${target.path}/lib/lib${name}.a`;
16
+ const isFresh = (dependency, name) => mtimeOf(`${dependency.paths.project}/${name}.xcframework/Info.plist`)
17
+ >= Math.max(...iosTargets.map((target) => mtimeOf(archiveOf(dependency, target, name))));
18
+ state.config.allDependencies.filter((dependency) => dependency.general.conan).forEach((dependency) => {
19
+ // A library its recipe names on another platform only has no slice here.
20
+ const libName = dependency.export.libName.filter((name) => iosTargets.every((target) => fs.existsSync(archiveOf(dependency, target, name))));
21
+ if (libName.every((name) => isFresh(dependency, name))) return;
22
+ createXCFramework({
23
+ paths: { project: dependency.paths.project, output: dependency.paths.output },
24
+ export: { libName },
25
+ targetParams: {
26
+ platform: ['ios'], arch: iosTargets.map((target) => target.arch), runtime: ['mt'], buildType: ['release'],
27
+ },
28
+ });
29
+ });
30
+ }
31
+
32
+ // Conan packages are staged before any header is read: SWIG parses headers that include theirs, and an
33
+ // interface made without its includes stays that way until the header itself changes. Release trees
34
+ // only - a debug build links them like any prebuilt.
35
+ export default async function prepareConanDependencies(targets) {
36
+ if (Object.keys(state.config.conanDependencies ?? {}).length === 0) return;
37
+ // createBridgeFile reads every header for the first wasm target unless it is told otherwise, as it
38
+ // is by Metro, which builds the React Native bridges for the platform's own target, and by a Node
39
+ // build, which reads them for one of its own (nodeBridgeTarget).
40
+ const bridgeTarget = targets.some((target) => target.platform === 'wasm') && state.targets.find((target) => target.platform === 'wasm');
41
+ // One xcframework holds both SDKs, so an iOS build stages both.
42
+ const iosTargets = targets.some((target) => target.platform === 'ios') ? state.targets.filter((target) => target.platform === 'ios') : [];
43
+ const releaseTargets = [...new Map([...targets, bridgeTarget, ...iosTargets].filter(Boolean).map((target) => [
44
+ target.releasePath, releaseTargetOf(target),
45
+ ])).values()].filter(isBuiltHere);
46
+ await installConanPackages(state.config, releaseTargets);
47
+ // Also when another process did the staging: this one may have loaded before the manifests existed.
48
+ refreshConanDependencies(state.config);
49
+ setAllDependecyPaths();
50
+ if (iosTargets.length > 0) makeConanXCFrameworks(releaseTargets.filter((target) => target.platform === 'ios'));
51
+ }