crossbind 2.0.0-beta.62 → 2.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +15 -2
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildLib.js +25 -8
  6. package/src/actions/buildNativeCommand.js +84 -0
  7. package/src/actions/buildNode.js +48 -7
  8. package/src/actions/buildTypes.js +8 -2
  9. package/src/actions/buildWasiCommand.js +0 -4
  10. package/src/actions/buildWasm.js +49 -29
  11. package/src/actions/cloudCommands.js +133 -0
  12. package/src/actions/createImportedBridges.js +64 -0
  13. package/src/actions/createInterface.js +247 -68
  14. package/src/actions/createLib.js +57 -41
  15. package/src/actions/getData.js +4 -1
  16. package/src/actions/getLinkInputs.js +12 -5
  17. package/src/actions/isSourceNewer.js +2 -0
  18. package/src/actions/licenses.js +143 -19
  19. package/src/actions/prepareConanDependencies.js +51 -0
  20. package/src/actions/publishBinary.js +32 -0
  21. package/src/actions/run.js +63 -66
  22. package/src/actions/runnerCommands.js +421 -0
  23. package/src/actions/writeRuntimeEntry.js +69 -0
  24. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  25. package/src/assets/cmake/command/command.cpp +1 -0
  26. package/src/assets/cmake/dist.cmake +2 -1
  27. package/src/assets/configure/install.sh +22 -0
  28. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  29. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  30. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -60
  31. package/src/assets/js-runtime/core.js +11 -0
  32. package/src/assets/node-import-hooks.mjs +61 -0
  33. package/src/assets/toolchain-digests.json +19 -19
  34. package/src/assets/wasi-runtime/stubs.c +29 -0
  35. package/src/bin.js +257 -50
  36. package/src/index.js +3 -0
  37. package/src/integration/bindHeaderImports.js +18 -0
  38. package/src/integration/getCrossbindScript.js +46 -10
  39. package/src/integration/getDependFilePath.js +9 -0
  40. package/src/integration/resolveNativeImport.js +14 -0
  41. package/src/node/dev.mjs +19 -0
  42. package/src/runner/blobs.js +135 -0
  43. package/src/runner/files.js +193 -0
  44. package/src/runner/server.js +320 -0
  45. package/src/state/attachConanDependencies.js +56 -0
  46. package/src/state/index.js +17 -9
  47. package/src/state/loadConfig.js +33 -24
  48. package/src/state/refreshBuiltDependencies.js +20 -0
  49. package/src/state/refreshConanDependencies.js +17 -0
  50. package/src/utils/androidToolchain.js +4 -0
  51. package/src/utils/appRustCrates.js +15 -1
  52. package/src/utils/appSources.js +97 -6
  53. package/src/utils/archiveFlags.js +12 -0
  54. package/src/utils/boundHeaders.js +79 -0
  55. package/src/utils/bridgeExtras.js +486 -0
  56. package/src/utils/bridgeRuntimeFixes.js +48 -0
  57. package/src/utils/bundledLicenses.js +26 -0
  58. package/src/utils/cargoTarget.js +21 -3
  59. package/src/utils/cloudAccount.js +55 -0
  60. package/src/utils/conanDependencies.js +66 -0
  61. package/src/utils/conanImport.js +58 -0
  62. package/src/utils/conanInstall.js +154 -0
  63. package/src/utils/conanProfile.js +203 -0
  64. package/src/utils/conanStage.js +292 -0
  65. package/src/utils/copyToDistSource.js +13 -0
  66. package/src/utils/cppDts.js +58 -15
  67. package/src/utils/darwinToolchain.js +11 -0
  68. package/src/utils/dirLock.js +137 -28
  69. package/src/utils/downloadAndExtractFile.js +34 -7
  70. package/src/utils/embindArgumentGuards.js +41 -2
  71. package/src/utils/embindCallArguments.js +19 -0
  72. package/src/utils/embindJspiQueue.js +25 -0
  73. package/src/utils/execContainer.js +49 -0
  74. package/src/utils/headerImports.js +16 -5
  75. package/src/utils/hostPlatform.js +21 -0
  76. package/src/utils/iosToolchain.js +6 -0
  77. package/src/utils/isSourceCmakePackage.js +10 -0
  78. package/src/utils/licenseReport.js +11 -2
  79. package/src/utils/linuxToolchain.js +24 -0
  80. package/src/utils/makeTreeWritable.js +12 -0
  81. package/src/utils/nativeImports.js +23 -0
  82. package/src/utils/nativeSourceStamp.js +36 -0
  83. package/src/utils/nodeDevBuild.js +92 -0
  84. package/src/utils/packageLicense.js +101 -0
  85. package/src/utils/pthreadMainScript.js +14 -0
  86. package/src/utils/pullDockerImage.js +3 -1
  87. package/src/utils/relocatePrebuilt.js +119 -0
  88. package/src/utils/remoteClient.js +127 -0
  89. package/src/utils/remoteOutputs.js +39 -0
  90. package/src/utils/remoteRunner.js +97 -0
  91. package/src/utils/remoteUpload.js +134 -0
  92. package/src/utils/replaceFile.js +8 -0
  93. package/src/utils/runCargo.js +48 -51
  94. package/src/utils/runConan.js +257 -0
  95. package/src/utils/runtimeEntries.js +56 -0
  96. package/src/utils/rustBridgeGen.js +9 -4
  97. package/src/utils/rustMt.js +4 -0
  98. package/src/utils/scopedEmbind.js +22 -0
  99. package/src/utils/selectRunner.js +81 -0
  100. package/src/utils/sourceFingerprint.js +4 -3
  101. package/src/utils/swigInterface.js +69 -4
  102. package/src/utils/systemKeys.js +35 -3
  103. package/src/utils/targets.js +49 -4
  104. package/src/utils/toolchainNotices.js +5 -0
  105. package/src/utils/wasiToolchain.js +14 -1
  106. package/src/utils/windowsToolchain.js +18 -0
  107. package/vitest.config.js +11 -0
@@ -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'];
@@ -29,6 +32,11 @@ 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
 
@@ -40,10 +48,15 @@ export default function createLib(target, fileType, options = {}) {
40
48
  const fingerprintChanged = fingerprint !== null
41
49
  && (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
42
50
 
43
- // 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.
44
53
  const configEmccFlags = getData('binary', target)?.emccFlags || [];
54
+ const compileOptions = getData('cmake', target)?.compileOptions || [];
45
55
  const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
46
- 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));
47
60
  const flagsChanged = !fs.existsSync(flagsFingerprintFile)
48
61
  || fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
49
62
 
@@ -65,7 +78,7 @@ export default function createLib(target, fileType, options = {}) {
65
78
  return changed;
66
79
  }
67
80
 
68
- if (!options.force && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
81
+ if (!options.force && !sourcesChanged && !fingerprintChanged && !flagsChanged && fs.existsSync(`${libdir}/lib`)) {
69
82
  logger.cachedStep(target, fileType);
70
83
  return false;
71
84
  }
@@ -79,7 +92,6 @@ export default function createLib(target, fileType, options = {}) {
79
92
 
80
93
  const buildEnv = { params: [] };
81
94
  let buildParams;
82
- const depPaths = state.config.allDependencyPaths[target.path];
83
95
  if (state.config.build.withBuildConfig) {
84
96
  const { getBuildParams, getExtraLibs } = state.config.build;
85
97
  buildEnv.console = true;
@@ -106,31 +118,20 @@ export default function createLib(target, fileType, options = {}) {
106
118
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
107
119
 
108
120
  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
- }
121
+ const archiveFlags = targetArchiveFlags(target);
122
+ cFlags.push(...archiveFlags);
123
+ ldFlags.push(...archiveFlags);
118
124
 
119
125
  if (isJspiTarget) {
120
126
  cFlags.push('-DCROSSBIND_JSPI');
121
127
  }
122
128
 
123
- if (target.platform === 'wasm' && target.arch === 'wasm64') {
124
- cFlags.push('-sMEMORY64=1');
125
- ldFlags.push('-sMEMORY64=1');
126
- }
127
-
128
129
  buildEnv.params.push('-e', `CFLAGS=${cFlags.join(' ')}`);
129
130
  buildEnv.params.push('-e', `CXXFLAGS=${cFlags.join(' ')}`);
130
131
  buildEnv.params.push('-e', `LDFLAGS=${ldFlags.join(' ')} ${extraLibs.join(' ')}`);
131
132
  // Emulation archives must land in LIBS (after the objects), not LDFLAGS.
132
133
  const wasiStubLib = state.config.build?.buildType === 'configure' ? `${wasiStubObj} ` : '';
133
- const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')}` : '';
134
+ const wasiLibs = target.platform === 'wasi' ? ` ${wasiStubLib}${WASI_EMULATION_LIBS.join(' ')} -Wl,--wrap=lseek` : '';
134
135
  buildEnv.params.push('-e', `LIBS=${dependLibs} ${extraLibs.join(' ')}${wasiLibs}`);
135
136
 
136
137
  let configBuildEnv = state.config.build.env;
@@ -145,22 +146,11 @@ export default function createLib(target, fileType, options = {}) {
145
146
 
146
147
  triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
147
148
 
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`);
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}`);
164
154
  }
165
155
 
166
156
  if (isJspiTarget) {
@@ -171,7 +161,17 @@ export default function createLib(target, fileType, options = {}) {
171
161
 
172
162
  logger.startStep(target, fileType);
173
163
  const t0 = performance.now();
174
- 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
+ }
175
175
 
176
176
  if (state.config.build?.beforeRun) {
177
177
  const dataList = state.config.build?.beforeRun(cmakeDir);
@@ -191,11 +191,24 @@ export default function createLib(target, fileType, options = {}) {
191
191
  ], platformPrefix, target, { console: buildEnv.console });
192
192
  };
193
193
 
194
- 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) {
195
205
  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 }) => {
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 }) => {
199
212
  replace({
200
213
  regex, replacement, paths: paths.map((p) => `${buildPath}/${p}`), recursive: false, silent: true,
201
214
  });
@@ -232,8 +245,10 @@ export default function createLib(target, fileType, options = {}) {
232
245
  // Some upstream makefiles race when one -j invocation carries multiple goals (openssl's
233
246
  // install builds apps twice); recipes can split phases via build.makePhases.
234
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()}`] : [];
235
250
  makePhases.forEach((phase) => {
236
- 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 });
237
252
  });
238
253
  }
239
254
  const t2 = performance.now();
@@ -247,6 +262,7 @@ export default function createLib(target, fileType, options = {}) {
247
262
  fs.writeFileSync(fingerprintFile, fingerprint);
248
263
  }
249
264
  fs.writeFileSync(flagsFingerprintFile, flagsFingerprint);
265
+ writeNativeSourceStamp(`${libdir}/lib`, sourceDirs);
250
266
  // Callers can force the final link when a lib actually rebuilt.
251
267
  return true;
252
268
  }
@@ -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') {
@@ -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
+ }