crossbind 2.0.0-beta.62 → 2.0.0-beta.64

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 (132) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +2 -2
  3. package/package.json +7 -2
  4. package/src/actions/buildBinTools.js +0 -0
  5. package/src/actions/buildDependencies.js +15 -2
  6. package/src/actions/buildExternal.js +9 -1
  7. package/src/actions/buildJs.js +2 -9
  8. package/src/actions/buildLib.js +25 -8
  9. package/src/actions/buildNativeCommand.js +84 -0
  10. package/src/actions/buildNode.js +49 -10
  11. package/src/actions/buildTypes.js +8 -2
  12. package/src/actions/buildWasiCommand.js +10 -9
  13. package/src/actions/buildWasm.js +72 -31
  14. package/src/actions/cloudCommands.js +133 -0
  15. package/src/actions/createImportedBridges.js +71 -0
  16. package/src/actions/createInterface.js +247 -68
  17. package/src/actions/createLib.js +70 -42
  18. package/src/actions/getData.js +6 -2
  19. package/src/actions/getEnv.js +7 -0
  20. package/src/actions/getLinkInputs.js +12 -5
  21. package/src/actions/isSourceNewer.js +2 -0
  22. package/src/actions/licenses.js +143 -19
  23. package/src/actions/prepareConanDependencies.js +51 -0
  24. package/src/actions/publishBinary.js +32 -0
  25. package/src/actions/run.js +63 -66
  26. package/src/actions/runnerCommands.js +433 -0
  27. package/src/actions/writeRuntimeEntry.js +80 -0
  28. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  29. package/src/assets/cmake/command/command.cpp +1 -0
  30. package/src/assets/cmake/dist.cmake +2 -1
  31. package/src/assets/configure/install.sh +22 -0
  32. package/src/assets/cpp-runtime/node.cpp +18 -1
  33. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  34. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  35. package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
  36. package/src/assets/js-runtime/core.js +12 -0
  37. package/src/assets/node-import-hooks.mjs +61 -0
  38. package/src/assets/toolchain-digests.json +19 -19
  39. package/src/assets/wasi-runtime/http/README.md +17 -0
  40. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  41. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  42. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  43. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  44. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  46. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  47. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  48. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  49. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  50. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  51. package/src/assets/wasi-runtime/stubs.c +29 -0
  52. package/src/bin.js +218 -161
  53. package/src/cli.js +14 -0
  54. package/src/index.js +3 -0
  55. package/src/integration/bindHeaderImports.js +18 -0
  56. package/src/integration/getCrossbindScript.js +50 -13
  57. package/src/integration/getDependFilePath.js +9 -0
  58. package/src/integration/resolveNativeImport.js +14 -0
  59. package/src/node/dev.mjs +19 -0
  60. package/src/program.js +169 -0
  61. package/src/runner/blobs.js +135 -0
  62. package/src/runner/files.js +193 -0
  63. package/src/runner/server.js +320 -0
  64. package/src/state/attachConanDependencies.js +56 -0
  65. package/src/state/index.js +20 -11
  66. package/src/state/loadConfig.js +37 -35
  67. package/src/state/refreshBuiltDependencies.js +20 -0
  68. package/src/state/refreshConanDependencies.js +17 -0
  69. package/src/utils/androidToolchain.js +4 -0
  70. package/src/utils/appRustCrates.js +19 -1
  71. package/src/utils/appSources.js +97 -6
  72. package/src/utils/archiveFlags.js +12 -0
  73. package/src/utils/boundHeaders.js +79 -0
  74. package/src/utils/bridgeExtras.js +554 -0
  75. package/src/utils/bridgeRuntimeFixes.js +48 -0
  76. package/src/utils/bundledLicenses.js +26 -0
  77. package/src/utils/cargoLock.js +19 -0
  78. package/src/utils/cargoTarget.js +21 -3
  79. package/src/utils/cloudAccount.js +55 -0
  80. package/src/utils/conanDependencies.js +66 -0
  81. package/src/utils/conanImport.js +58 -0
  82. package/src/utils/conanInstall.js +154 -0
  83. package/src/utils/conanProfile.js +203 -0
  84. package/src/utils/conanStage.js +292 -0
  85. package/src/utils/copyToDistSource.js +13 -0
  86. package/src/utils/cppDts.js +60 -15
  87. package/src/utils/darwinToolchain.js +11 -0
  88. package/src/utils/dirLock.js +137 -28
  89. package/src/utils/downloadAndExtractFile.js +34 -7
  90. package/src/utils/embindArgumentGuards.js +41 -2
  91. package/src/utils/embindCallArguments.js +19 -0
  92. package/src/utils/embindJspiQueue.js +25 -0
  93. package/src/utils/embindObjectLifetime.js +35 -0
  94. package/src/utils/execContainer.js +49 -0
  95. package/src/utils/headerImports.js +16 -5
  96. package/src/utils/hostPlatform.js +21 -0
  97. package/src/utils/iosToolchain.js +6 -0
  98. package/src/utils/isSourceCmakePackage.js +10 -0
  99. package/src/utils/licenseReport.js +11 -2
  100. package/src/utils/linuxToolchain.js +24 -0
  101. package/src/utils/makeTreeWritable.js +12 -0
  102. package/src/utils/nativeImports.js +23 -0
  103. package/src/utils/nativeSourceStamp.js +36 -0
  104. package/src/utils/nodeDevBuild.js +92 -0
  105. package/src/utils/packageLicense.js +103 -0
  106. package/src/utils/pthreadMainScript.js +14 -0
  107. package/src/utils/pullDockerImage.js +3 -1
  108. package/src/utils/relocatePrebuilt.js +119 -0
  109. package/src/utils/remoteClient.js +127 -0
  110. package/src/utils/remoteOutputs.js +39 -0
  111. package/src/utils/remoteRunner.js +97 -0
  112. package/src/utils/remoteUpload.js +134 -0
  113. package/src/utils/replaceFile.js +8 -0
  114. package/src/utils/resetCMakeCacheForSource.js +26 -0
  115. package/src/utils/runCargo.js +48 -51
  116. package/src/utils/runConan.js +257 -0
  117. package/src/utils/runtimeEntries.js +56 -0
  118. package/src/utils/rustBridgeGen.js +22 -8
  119. package/src/utils/rustMt.js +9 -0
  120. package/src/utils/rustSysroot.js +7 -5
  121. package/src/utils/scopedEmbind.js +22 -0
  122. package/src/utils/selectRunner.js +81 -0
  123. package/src/utils/sourceFingerprint.js +4 -3
  124. package/src/utils/swigInterface.js +70 -4
  125. package/src/utils/systemConfig.js +13 -0
  126. package/src/utils/systemKeys.js +35 -3
  127. package/src/utils/targets.js +60 -4
  128. package/src/utils/toolchainNotices.js +5 -0
  129. package/src/utils/wasiHttp.js +50 -0
  130. package/src/utils/wasiToolchain.js +14 -1
  131. package/src/utils/windowsToolchain.js +18 -0
  132. package/vitest.config.js +11 -0
@@ -10,8 +10,13 @@ import triggerExtensions from './extensions.js';
10
10
  import state from '../state/index.js';
11
11
  import logger from '../utils/logger.js';
12
12
  import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
13
+ import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
13
14
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
14
15
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
16
+ import { targetArchiveFlags } from '../utils/archiveFlags.js';
17
+ import { conanInputsOf } from '../utils/conanDependencies.js';
18
+ import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
19
+ import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
15
20
 
16
21
  const cpuCount = Math.max(1, os.cpus().length - 1);
17
22
  const sharedPlatforms = ['android'];
@@ -29,6 +34,11 @@ export default function createLib(target, fileType, options = {}) {
29
34
  const platformPrefix = `${fileType ? `${fileType}-` : ''}${buildType}`;
30
35
  const libdir = `${state.config.paths.build}/${platformPrefix}/prebuilt/${target.path}`;
31
36
  const buildPath = `${state.config.paths.build}/${platformPrefix}/${target.path}`;
37
+ const sourceDirs = options.buildSource ? state.config.paths.native : [];
38
+ const sourcesChanged = nativeSourceStampChanged(`${libdir}/lib`, sourceDirs);
39
+ // Automatically loaded cargo packages have no CMake dependency paths.
40
+ const depPaths = state.config.allDependencyPaths?.[target.path];
41
+ const sourceReplacements = state.config.build?.sourceReplaceList?.(target, depPaths) ?? [];
32
42
  // wasi configure tool links need the runtime stubs; compiled below, referenced from LIBS.
33
43
  const wasiStubObj = `${buildPath}/crossbind-wasi-stubs.o`;
34
44
 
@@ -40,10 +50,20 @@ export default function createLib(target, fileType, options = {}) {
40
50
  const fingerprintChanged = fingerprint !== null
41
51
  && (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
42
52
 
43
- // Config emccFlags feed compile-time state too (CROSSBIND_JSPI below), so a flag change must miss this cache.
53
+ // Config emccFlags feed compile-time state too (CROSSBIND_JSPI below), so a flag change must miss this cache, and so
54
+ // does a compile option a dependency declares (Lerc's LERC_STATIC on Windows). Without one the hash stays as it was.
44
55
  const configEmccFlags = getData('binary', target)?.emccFlags || [];
56
+ const compileOptions = getData('cmake', target)?.compileOptions || [];
45
57
  const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
46
- const flagsFingerprint = getContentHash(JSON.stringify(configEmccFlags));
58
+ const conanInputs = conanInputsOf(state.config);
59
+ const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
60
+ const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
61
+ const keyInputs = conanInputs ? [flagsInputs, conanInputs] : flagsInputs;
62
+ // An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
63
+ const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
64
+ ? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
65
+ : null;
66
+ const flagsFingerprint = getContentHash(JSON.stringify(httpHeader ? [keyInputs, httpHeader] : keyInputs));
47
67
  const flagsChanged = !fs.existsSync(flagsFingerprintFile)
48
68
  || fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
49
69
 
@@ -65,7 +85,7 @@ export default function createLib(target, fileType, options = {}) {
65
85
  return changed;
66
86
  }
67
87
 
68
- if (!options.force && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
88
+ if (!options.force && !sourcesChanged && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
69
89
  logger.cachedStep(target, fileType);
70
90
  return false;
71
91
  }
@@ -79,7 +99,6 @@ export default function createLib(target, fileType, options = {}) {
79
99
 
80
100
  const buildEnv = { params: [] };
81
101
  let buildParams;
82
- const depPaths = state.config.allDependencyPaths[target.path];
83
102
  if (state.config.build.withBuildConfig) {
84
103
  const { getBuildParams, getExtraLibs } = state.config.build;
85
104
  buildEnv.console = true;
@@ -106,31 +125,20 @@ export default function createLib(target, fileType, options = {}) {
106
125
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
107
126
 
108
127
  triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
109
- if (target.runtime === 'mt') {
110
- cFlags.push('-pthread');
111
- ldFlags.push('-pthread');
112
- }
113
-
114
- if (target.platform === 'wasm') {
115
- cFlags.push('-msimd128');
116
- ldFlags.push('-msimd128');
117
- }
128
+ const archiveFlags = targetArchiveFlags(target);
129
+ cFlags.push(...archiveFlags);
130
+ ldFlags.push(...archiveFlags);
118
131
 
119
132
  if (isJspiTarget) {
120
133
  cFlags.push('-DCROSSBIND_JSPI');
121
134
  }
122
135
 
123
- if (target.platform === 'wasm' && target.arch === 'wasm64') {
124
- cFlags.push('-sMEMORY64=1');
125
- ldFlags.push('-sMEMORY64=1');
126
- }
127
-
128
136
  buildEnv.params.push('-e', `CFLAGS=${cFlags.join(' ')}`);
129
137
  buildEnv.params.push('-e', `CXXFLAGS=${cFlags.join(' ')}`);
130
138
  buildEnv.params.push('-e', `LDFLAGS=${ldFlags.join(' ')} ${extraLibs.join(' ')}`);
131
139
  // Emulation archives must land in LIBS (after the objects), not LDFLAGS.
132
140
  const wasiStubLib = state.config.build?.buildType === 'configure' ? `${wasiStubObj} ` : '';
133
- const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')}` : '';
141
+ const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')} -Wl,--wrap=lseek` : '';
134
142
  buildEnv.params.push('-e', `LIBS=${dependLibs} ${extraLibs.join(' ')}${wasiLibs}`);
135
143
 
136
144
  let configBuildEnv = state.config.build.env;
@@ -141,26 +149,19 @@ export default function createLib(target, fileType, options = {}) {
141
149
  buildEnv.params.push('-e', e);
142
150
  });
143
151
  } else {
144
- buildParams = getCmakeParameters(target, { ...options, nativeGlob });
152
+ // The source of an http build includes <crossbind/http.h>.
153
+ const headerDirs = target.platform === 'wasi'
154
+ ? [...(options.headerDirs ?? []), stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)]
155
+ : options.headerDirs;
156
+ buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
145
157
 
146
158
  triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
147
159
 
148
- if (target.runtime === 'mt') {
149
- buildEnv.params.push('-e', `CFLAGS=-pthread`);
150
- buildEnv.params.push('-e', `CXXFLAGS=-pthread`);
151
- buildEnv.params.push('-e', `LDFLAGS=-pthread`);
152
- }
153
-
154
- if (target.platform === 'wasm') {
155
- buildEnv.params.push('-e', `CFLAGS=-msimd128`);
156
- buildEnv.params.push('-e', `CXXFLAGS=-msimd128`);
157
- buildEnv.params.push('-e', `LDFLAGS=-msimd128`);
158
- }
159
-
160
- if (target.platform === 'wasm' && target.arch === 'wasm64') {
161
- buildEnv.params.push('-e', `CFLAGS=-sMEMORY64=1`);
162
- buildEnv.params.push('-e', `CXXFLAGS=-sMEMORY64=1`);
163
- buildEnv.params.push('-e', `LDFLAGS=-sMEMORY64=1`);
160
+ const archiveFlags = targetArchiveFlags(target).join(' ');
161
+ if (archiveFlags) {
162
+ buildEnv.params.push('-e', `CFLAGS=${archiveFlags}`);
163
+ buildEnv.params.push('-e', `CXXFLAGS=${archiveFlags}`);
164
+ buildEnv.params.push('-e', `LDFLAGS=${archiveFlags}`);
164
165
  }
165
166
 
166
167
  if (isJspiTarget) {
@@ -171,7 +172,17 @@ export default function createLib(target, fileType, options = {}) {
171
172
 
172
173
  logger.startStep(target, fileType);
173
174
  const t0 = performance.now();
174
- const cmakeDir = state.config.build.withBuildConfig ? `${state.config.paths.build}/source` : state.config.paths.cmakeDir;
175
+ let cmakeDir = state.config.build.withBuildConfig ? `${state.config.paths.build}/source` : state.config.paths.cmakeDir;
176
+ if (state.config.build?.buildType !== 'configure' && sourceReplacements.length) {
177
+ // Patches belong to a target's copy: a platform-specific change must not alter another target's inputs.
178
+ const patchedSource = `${buildPath}/crossbind-source`;
179
+ fs.mkdirSync(buildPath, { recursive: true });
180
+ fs.cpSync(cmakeDir, patchedSource, { recursive: true, preserveTimestamps: true });
181
+ sourceReplacements.forEach(({ regex, replacement, paths }) => {
182
+ replace({ regex, replacement, paths: paths.map((p) => `${patchedSource}/${p}`), recursive: false, silent: true });
183
+ });
184
+ cmakeDir = patchedSource;
185
+ }
175
186
 
176
187
  if (state.config.build?.beforeRun) {
177
188
  const dataList = state.config.build?.beforeRun(cmakeDir);
@@ -191,11 +202,24 @@ export default function createLib(target, fileType, options = {}) {
191
202
  ], platformPrefix, target, { console: buildEnv.console });
192
203
  };
193
204
 
194
- if (!options.bypassCmake) {
205
+ // A configure build's make install runs coreutils install, whose chmod on a file it has just written
206
+ // Docker Desktop's file sharing now and then refuses; the wrapper finishes it by path. Staged like stubs.c.
207
+ const stageInstall = () => {
208
+ const staged = `${buildPath}/crossbind-install.sh`;
209
+ fs.mkdirSync(buildPath, { recursive: true });
210
+ fs.copyFileSync(`${state.config.paths.cli}/assets/configure/install.sh`, staged);
211
+ fs.chmodSync(staged, 0o755);
212
+ return staged;
213
+ };
214
+
215
+ if (!options.bypassCmake || sourcesChanged) {
195
216
  if (state.config.build?.buildType === 'configure') {
196
- fs.cpSync(cmakeDir, buildPath, { recursive: true });
197
- if (state.config.build?.sourceReplaceList) {
198
- state.config.build.sourceReplaceList(target, depPaths)?.forEach(({ regex, replacement, paths }) => {
217
+ // make compares times, so the tree starts afresh with the extracted times: no source then looks newer
218
+ // than what it generates, and no object of an earlier build looks newer than its source.
219
+ fs.rmSync(buildPath, { recursive: true, force: true });
220
+ fs.cpSync(cmakeDir, buildPath, { recursive: true, preserveTimestamps: true });
221
+ if (sourceReplacements.length) {
222
+ sourceReplacements.forEach(({ regex, replacement, paths }) => {
199
223
  replace({
200
224
  regex, replacement, paths: paths.map((p) => `${buildPath}/${p}`), recursive: false, silent: true,
201
225
  });
@@ -216,6 +240,7 @@ export default function createLib(target, fileType, options = {}) {
216
240
  stageWasiStubs();
217
241
  buildParams.push(`-DCMAKE_EXE_LINKER_FLAGS=${[...wasiCxxFlags(), wasiStubObj, ...WASI_LINK_LIBS].join(' ')}`);
218
242
  }
243
+ resetCMakeCacheForSource(buildPath, cmakeDir, target);
219
244
  run(null, [
220
245
  target.platform === 'ios' && state.config.build?.useIOSCMake ? 'ios-cmake' : 'cmake', cmakeDir,
221
246
  `-DCMAKE_BUILD_TYPE=${buildType}`,
@@ -232,8 +257,10 @@ export default function createLib(target, fileType, options = {}) {
232
257
  // Some upstream makefiles race when one -j invocation carries multiple goals (openssl's
233
258
  // install builds apps twice); recipes can split phases via build.makePhases.
234
259
  const makePhases = state.config.build?.makePhases || [['install']];
260
+ // A make command-line variable beats the Makefile's own INSTALL, autosetup's (sqlite3) included.
261
+ const installParams = state.config.build?.buildType === 'configure' ? [`INSTALL=${stageInstall()}`] : [];
235
262
  makePhases.forEach((phase) => {
236
- run(null, ['make', `-j${cpuCount}`, ...phase], platformPrefix, target, { console: buildEnv.console });
263
+ run(null, ['make', `-j${cpuCount}`, ...installParams, ...phase], platformPrefix, target, { console: buildEnv.console, exclusive: true });
237
264
  });
238
265
  }
239
266
  const t2 = performance.now();
@@ -247,6 +274,7 @@ export default function createLib(target, fileType, options = {}) {
247
274
  fs.writeFileSync(fingerprintFile, fingerprint);
248
275
  }
249
276
  fs.writeFileSync(flagsFingerprintFile, flagsFingerprint);
277
+ writeNativeSourceStamp(`${libdir}/lib`, sourceDirs);
250
278
  // Callers can force the final link when a lib actually rebuilt.
251
279
  return true;
252
280
  }
@@ -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') {
@@ -27,7 +30,8 @@ function getRecursiveData(obj, config, dependency, field, target) {
27
30
  obj[dKey] = [...(obj[dKey] || []), ...value];
28
31
  } else if (typeof value === 'object') {
29
32
  obj[dKey] = { ...(obj[dKey] || {}), ...value };
30
- } else {
33
+ } else if (!Object.hasOwn(obj, dKey)) {
34
+ // The app comes before its dependencies and a package before its own, so the nearest value stays.
31
35
  obj[dKey] = value;
32
36
  }
33
37
  }
@@ -0,0 +1,7 @@
1
+ import state from '../state/index.js';
2
+ import getData from './getData.js';
3
+
4
+ export default function getEnv(target) {
5
+ return Object.fromEntries(Object.entries(getData('env', target))
6
+ .map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
7
+ }
@@ -3,9 +3,10 @@ import getDependLibs from './getDependLibs.js';
3
3
  import state from '../state/index.js';
4
4
  import buildAppRustCrates from '../utils/appRustCrates.js';
5
5
 
6
- // The archives of a final link (wasm or Node-API addon) and which of them must survive whole.
7
- // keepFlag spells "keep this symbol" for the target's linker.
8
- export default function getLinkInputs(target, { keepFlag }) {
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 }) {
9
10
  const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
10
11
  const { build, output, cache } = state.config.paths;
11
12
  const { name } = state.config.general;
@@ -23,7 +24,7 @@ export default function getLinkInputs(target, { keepFlag }) {
23
24
  ...getDependLibs(target),
24
25
  ...appRustLibs,
25
26
  sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
26
- `${build}/Bridge-${buildType}/${target.path}/lib${name}.a`,
27
+ ...(withBridge ? [`${build}/Bridge-${buildType}/${target.path}/lib${name}.a`] : []),
27
28
  ];
28
29
 
29
30
  // By default only the Bridge archive is kept whole (see linkLayout.js) so the linker
@@ -38,8 +39,14 @@ export default function getLinkInputs(target, { keepFlag }) {
38
39
  // with their keep symbol pinned (keepFlag pulls just the registration object); manual-bindings
39
40
  // crates have no keep symbol and fall back to whole-archive - safe only while they are the
40
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.
41
45
  const rustKeepFlags = [];
42
- if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
46
+ if (appRustLibs.length > 0) {
47
+ if (target.platform === 'win32') rustKeepFlags.push(keepFlag('crossbind_app_super'));
48
+ else wholeArchiveNames.add('crossbind_app_super');
49
+ }
43
50
  const depends = state.config.dependencyParameters.getCmakeDepends(target);
44
51
  depends.forEach((dep) => {
45
52
  if (dep.export.wholeArchive === true) {
@@ -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,16 @@ 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
+
93
116
  // A Linux or Windows addon carries its C++ runtime; a Windows one also carries parts of the
94
117
  // mingw-w64 runtime and winpthreads, whose licenses ask for their notices in binary distributions.
95
118
  // A macOS addon uses the system's C++ runtime.
@@ -106,7 +129,7 @@ function addonToolchainRows(platform) {
106
129
  sourceUrl: 'https://github.com/llvm/llvm-project',
107
130
  };
108
131
  const llvmText = 'license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT';
109
- if (platform === 'linux') {
132
+ if (platform === 'linux' || platform === 'linuxmusl') {
110
133
  return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the addon; ${llvmText}` }];
111
134
  }
112
135
  const mingwLicense = 'LicenseRef-MinGW-w64-runtime';
@@ -119,8 +142,57 @@ function addonToolchainRows(platform) {
119
142
  licenseDeclared: mingwLicense,
120
143
  sourceUrl: 'https://github.com/mingw-w64/mingw-w64',
121
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')),
122
181
  },
123
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
+ }];
124
196
  }
125
197
 
126
198
  async function buildRow(node) {
@@ -143,12 +215,18 @@ async function buildRow(node) {
143
215
  let licenseText;
144
216
  if (upstream) {
145
217
  license = upstream.selected || upstream.declared;
146
- 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);
147
227
  } else {
148
228
  license = manifest?.license || null;
149
- const legacy = LEGACY_LICENSE_FILES
150
- .map((name) => `${node.paths.project}/${name}`)
151
- .find((file) => fs.existsSync(file));
229
+ const legacy = packageLicenseFile(node.paths.project);
152
230
  licenseText = legacy ? fs.readFileSync(legacy, 'utf8') : null;
153
231
  }
154
232
 
@@ -168,23 +246,69 @@ async function buildRow(node) {
168
246
  };
169
247
  }
170
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
+
171
284
  // platform (optional, e.g. 'wasi') additionally includes what that platform's
172
285
  // artifact statically links beyond the package graph: recipe-declared vendored
173
- // copies and the toolchain runtime.
174
- export default async function collectLicenseRows(platform = null) {
175
- // 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);
176
294
  const nodes = [state.config, ...state.config.allDependencies]
177
- .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)));
178
297
  const rows = [];
179
298
  const seen = new Set();
180
299
  for (const node of nodes) {
181
- const key = node.general.alias.package;
300
+ const key = keyOf(node);
182
301
  if (seen.has(key)) continue;
183
302
  seen.add(key);
184
- rows.push(await buildRow(node));
185
- 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
+ }
186
309
  }
187
310
  if (platform === 'wasi') rows.push(...wasiToolchainRows());
188
- if (platform === 'linux' || platform === 'win32') rows.push(...addonToolchainRows(platform));
311
+ if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
312
+ if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
189
313
  return rows.sort((a, b) => a.name.localeCompare(b.name));
190
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
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import state from '../state/index.js';
4
+ import replaceFile from '../utils/replaceFile.js';
5
+
6
+ // paths.output defaults to paths.build, where a built command and its data already are; copying them onto
7
+ // themselves would delete them first.
8
+ const outputIsBuild = () => path.resolve(state.config.paths.output) === path.resolve(state.config.paths.build);
9
+
10
+ export function publishNativeCommand(target) {
11
+ if (outputIsBuild()) return;
12
+ fs.mkdirSync(state.config.paths.output, { recursive: true });
13
+ replaceFile(`${state.config.paths.build}/${target.commandName}`, `${state.config.paths.output}/${target.commandName}`);
14
+ }
15
+
16
+ export function publishNodeAddon(target) {
17
+ if (outputIsBuild()) return;
18
+ fs.mkdirSync(state.config.paths.output, { recursive: true });
19
+ replaceFile(`${state.config.paths.build}/${target.addonName}`, `${state.config.paths.output}/${target.addonName}`);
20
+ fs.copyFileSync(`${state.config.paths.build}/${target.jsName}`, `${state.config.paths.output}/${target.jsName}`);
21
+ }
22
+
23
+ export function publishWasiCommand(target) {
24
+ if (outputIsBuild()) return;
25
+ fs.copyFileSync(`${state.config.paths.build}/${target.wasmName}`, `${state.config.paths.output}/${target.wasmName}`);
26
+ // Copied (not renamed) so a later fingerprint cache-hit still has the
27
+ // build-dir tree to serve from.
28
+ if (fs.existsSync(`${state.config.paths.build}/data`)) {
29
+ fs.rmSync(`${state.config.paths.output}/data`, { recursive: true, force: true });
30
+ fs.cpSync(`${state.config.paths.build}/data`, `${state.config.paths.output}/data`, { recursive: true });
31
+ }
32
+ }