crossbind 2.0.0-beta.60 → 2.0.0-beta.62

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 (43) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/actions/buildDependencies.js +22 -7
  4. package/src/actions/buildJs.js +1 -1
  5. package/src/actions/buildLib.js +2 -0
  6. package/src/actions/buildNode.js +149 -0
  7. package/src/actions/buildWasm.js +43 -66
  8. package/src/actions/createInterface.js +101 -27
  9. package/src/actions/createLib.js +3 -2
  10. package/src/actions/getAllBridges.js +1 -1
  11. package/src/actions/getLinkInputs.js +63 -0
  12. package/src/actions/licenses.js +34 -0
  13. package/src/actions/run.js +97 -2
  14. package/src/assets/cmake/CMakeLists.txt +1 -1
  15. package/src/assets/cmake/dist.cmake +1 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
  17. package/src/assets/toolchain-digests.json +26 -12
  18. package/src/bin.js +59 -13
  19. package/src/index.js +3 -1
  20. package/src/integration/getCrossbindScript.js +8 -10
  21. package/src/integration/getDependFilePath.js +9 -12
  22. package/src/state/index.js +16 -13
  23. package/src/state/loadConfig.js +3 -0
  24. package/src/utils/appRustCrates.js +2 -0
  25. package/src/utils/appSources.js +21 -0
  26. package/src/utils/cargoImport.js +44 -0
  27. package/src/utils/cargoTarget.js +2 -0
  28. package/src/utils/cppDts.js +77 -41
  29. package/src/utils/embindArgumentGuards.js +45 -0
  30. package/src/utils/embindRsFingerprint.js +26 -0
  31. package/src/utils/headerImports.js +45 -0
  32. package/src/utils/inputStamp.js +23 -0
  33. package/src/utils/linkLayout.js +8 -1
  34. package/src/utils/pullDockerImage.js +8 -5
  35. package/src/utils/resolveCompanionPackage.js +51 -0
  36. package/src/utils/resolveEmbindNapi.js +22 -0
  37. package/src/utils/resolveEmbindRust.js +17 -52
  38. package/src/utils/runCargo.js +4 -3
  39. package/src/utils/rustBridgeGen.js +895 -250
  40. package/src/utils/swigInterface.js +19 -4
  41. package/src/utils/systemKeys.js +8 -0
  42. package/src/utils/targets.js +56 -0
  43. package/src/utils/cppFieldBindings.js +0 -23
@@ -1,11 +1,13 @@
1
1
 
2
2
  import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import upath from 'upath';
4
5
  import state, { saveCache } from '../state/index.js';
5
- import { getFileHash } from '../utils/hash.js';
6
+ import { getContentHash, getFileHash } from '../utils/hash.js';
6
7
  import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
7
- import { writeHeaderDts, parseCppSurface } from '../utils/cppDts.js';
8
- import { injectFieldBindings } from '../utils/cppFieldBindings.js';
8
+ import getDependFilePath from '../integration/getDependFilePath.js';
9
+ import { writeHeaderDts } from '../utils/cppDts.js';
10
+ import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
9
11
  import {
10
12
  buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, indexTypeDefinitions, interfaceIncludes,
11
13
  interfaceToRetryWithoutMacros, parseMacroDump, referencedTypeHeaders, selectSwigMacros,
@@ -15,6 +17,9 @@ import run, { cxxPreprocessorFor } from './run.js';
15
17
 
16
18
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
17
19
  const INTERFACE_FORMAT = 'swig-macros-1';
20
+ // Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
21
+ // bridges from a fork before field bindings and constants are rebuilt.
22
+ const BRIDGE_FORMAT = 'swig-constants-1';
18
23
  const predefinedMacros = new Map();
19
24
  const typeDefinitions = new Map();
20
25
 
@@ -32,13 +37,6 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
32
37
  const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir);
33
38
  const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, getFileHash(interfaceFilePath));
34
39
  const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
35
- if (bridgeFile && !moduleRegex.test(interfaceFilePath)) {
36
- // SWIG's -embind backend emits no member variables; inject .property lines for the
37
- // header's public value fields. Idempotent, so the cached-bridge path is safe too.
38
- const bridgeContent = fs.readFileSync(bridgeFile, 'utf8');
39
- const injected = injectFieldBindings(bridgeContent, parseCppSurface(fs.readFileSync(interfaceFilePath, 'utf8'), () => {}));
40
- if (injected !== bridgeContent) fs.writeFileSync(bridgeFile, injected);
41
- }
42
40
  if (bridgeFile) {
43
41
  // Records which header this bridge came from, so directory-driven consumers
44
42
  // (getAllBridges) can prune bridges whose header no longer exists.
@@ -77,14 +75,21 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
77
75
  const prelude = isModule ? [] : findHeaderPrelude(headerOrModuleFilePath, packages, headerPath);
78
76
  const ignored = isModule ? [] : findIgnoredDeclarations(headerOrModuleFilePath, packages, headerPath);
79
77
  const completing = includeRoot ? findCompletingIncludes(headerOrModuleFilePath, includeRoot, headerPath) : [];
80
- // A prelude or ignored-declaration change in the owning package, or a completed class moving to another header, must
81
- // regenerate the interface as well.
78
+ const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
79
+ const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
80
+ const constants = isModule ? [] : importedNames(headerOrModuleFilePath, target);
81
+ // A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
82
+ // interface the package ships beside the header changing or going away, or another imported name must regenerate
83
+ // the interface as well.
82
84
  const fileHash = [
83
85
  INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
84
86
  ...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
87
+ ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
88
+ ...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
85
89
  ].join('\n');
86
90
  const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
87
- if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)) {
91
+ if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
92
+ && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
88
93
  return cachedInterface;
89
94
  }
90
95
 
@@ -97,11 +102,7 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
97
102
  return newPath;
98
103
  }
99
104
 
100
- const temp = headerOrModuleFilePath.match(/^(.*)\..+?$/);
101
- if (!temp || temp.length < 2) return null;
102
-
103
- const filePathWithoutExt = temp[1];
104
- const interfaceFile = `${filePathWithoutExt}.i`;
105
+ if (!filePathWithoutExt) return null;
105
106
 
106
107
  if (fs.existsSync(interfaceFile)) {
107
108
  const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
@@ -112,15 +113,16 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
112
113
  return newPath;
113
114
  }
114
115
 
115
- const fileName = filePathWithoutExt.split('/').at(-1);
116
+ const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
116
117
 
117
118
  const content = buildInterfaceContent({
118
119
  moduleName: fileName.toUpperCase(),
119
120
  headerPath,
120
121
  prelude,
121
122
  completing,
122
- swigMacros: collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir),
123
+ swigMacros: collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants),
123
124
  ignored,
125
+ constants,
124
126
  });
125
127
  const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
126
128
  fs.writeFileSync(outputFilePath, content);
@@ -132,6 +134,21 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
132
134
  return outputFilePath;
133
135
  }
134
136
 
137
+ const interfacePath = (name) => `${state.config.paths.build}/interface/${name}.i`;
138
+ // Each platform build of a package ships its headers under dist/prebuilt/<target>/include: one header for the interface.
139
+ const headerIdentity = (file) => file.match(/\/dist\/prebuilt\/[^/]+\/include\/(.+)$/)?.[1] ?? file;
140
+ const sharedInterface = (headerFile, interfaceFile) => Object.entries(state.cache.interfaces)
141
+ .some(([other, file]) => headerIdentity(other) !== headerIdentity(headerFile) && file === interfaceFile);
142
+
143
+ // An interface is named after its header's file name. A second header with that name (tiffio.h and tiffio.hxx, or one
144
+ // name in two directories) takes its extension, then a hash of its path, so neither replaces the other's bridge.
145
+ function interfaceName(headerFile, base) {
146
+ const candidates = [base, `${base}_${upath.extname(headerFile).slice(1)}`, `${base}_${getContentHash(headerFile).slice(0, 8)}`];
147
+ return candidates.find((name) => !sharedInterface(headerFile, interfacePath(name))) ?? candidates.at(-1);
148
+ }
149
+
150
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
151
+
135
152
  // A header is included by its path under a project header directory or a dependency's include directory; one found only
136
153
  // through its own directory has no include root to search.
137
154
  function includeLocation(headerFile, target) {
@@ -139,7 +156,7 @@ function includeLocation(headerFile, target) {
139
156
  if (projectRoot) return { includeRoot: projectRoot, headerPath: headerFile.substr(projectRoot.length + 1) };
140
157
  const dependencyRoots = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
141
158
  .filter((d) => d.startsWith(state.config.paths.base));
142
- const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${p}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
159
+ const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${escapeRegExp(p)}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
143
160
  return match ? { includeRoot: match[1], headerPath: match[2] } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
144
161
  }
145
162
 
@@ -171,24 +188,70 @@ function dependencyHeadersOf(headerFile, target) {
171
188
  .map((header) => upath.join(includeRoot, header));
172
189
  }
173
190
 
191
+ // The proxy module's own exports, never names of the header.
192
+ const PROXY_NAMES = new Set(['initNative', 'AllSymbols']);
193
+ const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
194
+
195
+ // A package header resolves under the target's prebuilt directory, and the bundler may resolve it for another target.
196
+ const targetNeutral = (file) => file?.replace(/\/dist\/prebuilt\/[^/]+\/include\//, '/dist/prebuilt/*/include/');
197
+
198
+ function realPath(file) {
199
+ if (!file) return null;
200
+ try {
201
+ return upath.normalize(fs.realpathSync(file));
202
+ } catch (e) {
203
+ return upath.resolve(file);
204
+ }
205
+ }
206
+
207
+ // A package that is not a crossbind dependency (the conformance kit) resolves the way the bundler finds it.
208
+ function resolveHeaderImport(specifier, importer, target) {
209
+ if (specifier.startsWith('.')) return upath.resolve(upath.dirname(importer), specifier);
210
+ if (upath.isAbsolute(specifier)) return specifier;
211
+ try {
212
+ return getDependFilePath(specifier, target) ?? createRequire(importer).resolve(specifier);
213
+ } catch (e) {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ // The names the app's own sources import from this header, or every name for `import * as`.
219
+ function importedNames(headerFile, target) {
220
+ const projectDir = state.config.paths.project;
221
+ if (!projectDir || !fs.existsSync(projectDir)) return [];
222
+ const header = targetNeutral(realPath(headerFile));
223
+ const names = new Set();
224
+ for (const { importer, specifier, names: imported } of findHeaderImportsIn(projectDir, state.config.ext.header)) {
225
+ if (targetNeutral(realPath(resolveHeaderImport(specifier, importer, target))) !== header) continue;
226
+ if (imported === ALL_NAMES) return ALL_NAMES;
227
+ imported.filter((name) => C_IDENTIFIER.test(name) && !PROXY_NAMES.has(name)).forEach((name) => names.add(name));
228
+ }
229
+ return [...names].sort();
230
+ }
231
+
174
232
  // A header that does not preprocess on its own, or a host without the image's compiler, keeps the plain interface.
175
- function collectSwigMacros(headerFile, includes, name, target, sourceDir) {
233
+ function collectSwigMacros(headerFile, includes, name, target, sourceDir, constants) {
176
234
  const interfaceDir = `${state.config.paths.build}/interface`;
177
235
  try {
178
236
  if (!predefinedMacros.has(target.path)) {
179
237
  predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
180
238
  }
181
- const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target);
182
- return selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path) });
239
+ const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target, [RELEASE_DEFINE]);
240
+ return selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants });
183
241
  } catch (e) {
184
242
  console.warn(`crossbind: SWIG reads ${upath.basename(headerFile)} without the macros of its includes (${e.message})`);
185
243
  return [];
186
244
  }
187
245
  }
188
246
 
189
- function dumpMacros(outputFile, includes, includePath, target) {
247
+ // Bridges compile in release with NDEBUG defined, so SWIG reads headers the same way: a declaration that exists only
248
+ // without NDEBUG (sqlite3_mutex_held) would otherwise be bound and fail to compile. The predefined dump stays without
249
+ // it, so NDEBUG counts as a macro SWIG needs rather than a compiler predefine.
250
+ const RELEASE_DEFINE = '-DNDEBUG';
251
+
252
+ function dumpMacros(outputFile, includes, includePath, target, defines = []) {
190
253
  run(cxxPreprocessorFor(target), [
191
- '-x', 'c++', '-std=c++17', '-dM', '-E',
254
+ '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
192
255
  ...includePath,
193
256
  ...includes.flatMap((header) => ['-include', header]),
194
257
  '-o', outputFile,
@@ -221,6 +284,16 @@ function applyAsyncGuard(bridgeFilePath) {
221
284
  }
222
285
  }
223
286
 
287
+ // A build hides SWIG's output, so the SWIG fork writes each binding it skipped beside the bridge, under the path SWIG saw.
288
+ function reportSkippedBindings(bridgeFilePath) {
289
+ const warningsFile = `${bridgeFilePath}.warnings`;
290
+ if (!fs.existsSync(warningsFile)) return;
291
+ fs.readFileSync(warningsFile, 'utf8').split('\n').filter(Boolean).forEach((line) => {
292
+ const [, file, lineNumber, message] = line.match(/^(.*):(\d+): (.*)$/) ?? [];
293
+ console.warn(`crossbind: ${file ? `${upath.basename(file)}:${lineNumber}: ${message}` : line}`);
294
+ });
295
+ }
296
+
224
297
  // The interface text only names the header, so its hash alone kept a bridge across header edits: the
225
298
  // header's own hash is part of the key.
226
299
  function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '') {
@@ -228,7 +301,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
228
301
  return null;
229
302
  }
230
303
 
231
- const bridgeHash = () => `${getFileHash(interfaceFilePath)}\n${sourceHash}`;
304
+ const bridgeHash = () => `${BRIDGE_FORMAT}\n${getFileHash(interfaceFilePath)}\n${sourceHash}`;
232
305
  const fileHash = bridgeHash();
233
306
  const cachedBridge = state.cache.bridges[interfaceFilePath];
234
307
  if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
@@ -264,6 +337,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
264
337
  }
265
338
  }
266
339
  applyAsyncGuard(bridgeFilePath);
340
+ reportSkippedBindings(bridgeFilePath);
267
341
 
268
342
  state.cache.bridges[interfaceFilePath] = bridgeFilePath;
269
343
  state.cache.hashes[interfaceFilePath] = bridgeHash();
@@ -21,7 +21,7 @@ export default function createLib(target, fileType, options = {}) {
21
21
  throw new Error('invalid target or options');
22
22
  }
23
23
 
24
- if (target.platform === 'ios' && process.platform !== 'darwin') {
24
+ if ((target.platform === 'ios' || target.platform === 'darwin') && process.platform !== 'darwin') {
25
25
  return;
26
26
  }
27
27
 
@@ -35,7 +35,8 @@ export default function createLib(target, fileType, options = {}) {
35
35
  const nativeGlob = options.nativeGlob ? withDependencyBridges(options.nativeGlob) : undefined;
36
36
  // 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
37
  const fingerprintFile = `${libdir}/crossbind-nativeglob.fingerprint`;
38
- const fingerprint = nativeGlob ? getFilesFingerprint(nativeGlob) : null;
38
+ // options.inputs lists what the sources compile against (a binding runtime's headers), so a change there rebuilds too.
39
+ const fingerprint = nativeGlob ? getFilesFingerprint([...nativeGlob, ...(options.inputs ?? [])]) : null;
39
40
  const fingerprintChanged = fingerprint !== null
40
41
  && (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
41
42
 
@@ -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
  });
@@ -0,0 +1,63 @@
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 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 }) {
9
+ const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
10
+ const { build, output, cache } = state.config.paths;
11
+ const { name } = state.config.general;
12
+
13
+ // buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
14
+ // build-dir copy can be gone while the output artifact is still valid — link it then.
15
+ const sourceLibCandidates = [
16
+ `${build}/Source-${buildType}/${target.path}/lib${name}.a`,
17
+ `${output}/prebuilt/${target.path}/lib/lib${name}.a`,
18
+ ];
19
+ // App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
20
+ // single fully-loaded Rust archive of the link (see the libstd rule below).
21
+ const appRustLibs = buildAppRustCrates(target, cache, state.config.cargoDependencies ?? {});
22
+ const libs = [
23
+ ...getDependLibs(target),
24
+ ...appRustLibs,
25
+ sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
26
+ `${build}/Bridge-${buildType}/${target.path}/lib${name}.a`,
27
+ ];
28
+
29
+ // By default only the Bridge archive is kept whole (see linkLayout.js) so the linker
30
+ // dead-code-eliminates everything unreferenced. Two escape hatches, both
31
+ // `export.wholeArchive: true`: on the APP it restores the legacy layout (every archive
32
+ // wholesale); on a LIBRARY's own config it keeps that library's archives whole in every
33
+ // consumer link (for members that self-register from static initializers).
34
+ const wholeArchiveAll = state.config.export.wholeArchive === true;
35
+ const wholeArchiveNames = new Set();
36
+ // Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
37
+ // may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
38
+ // with their keep symbol pinned (keepFlag pulls just the registration object); manual-bindings
39
+ // crates have no keep symbol and fall back to whole-archive - safe only while they are the
40
+ // single loaded Rust archive.
41
+ const rustKeepFlags = [];
42
+ if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
43
+ const depends = state.config.dependencyParameters.getCmakeDepends(target);
44
+ depends.forEach((dep) => {
45
+ if (dep.export.wholeArchive === true) {
46
+ (dep.export.libName || []).forEach((libName) => wholeArchiveNames.add(libName));
47
+ }
48
+ if (dep.export.type === 'cargo') {
49
+ const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
50
+ const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
51
+ (dep.export.libName || []).forEach((libName) => {
52
+ if (isManual) wholeArchiveNames.add(libName);
53
+ else rustKeepFlags.push(keepFlag(libName));
54
+ });
55
+ }
56
+ });
57
+
58
+ const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
59
+ || depends.some((dep) => dep.export.type === 'cargo');
60
+ return {
61
+ libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
62
+ };
63
+ }
@@ -90,6 +90,39 @@ function wasiToolchainRows() {
90
90
  ];
91
91
  }
92
92
 
93
+ // A Linux or Windows addon carries its C++ runtime; a Windows one also carries parts of the
94
+ // mingw-w64 runtime and winpthreads, whose licenses ask for their notices in binary distributions.
95
+ // A macOS addon uses the system's C++ runtime.
96
+ function addonToolchainRows(platform) {
97
+ const shared = {
98
+ npmName: null, version: null, nativeVersion: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
99
+ };
100
+ const llvmLicense = 'Apache-2.0 WITH LLVM-exception';
101
+ const llvmRuntimes = {
102
+ ...shared,
103
+ name: 'llvm-runtimes',
104
+ license: llvmLicense,
105
+ licenseDeclared: llvmLicense,
106
+ sourceUrl: 'https://github.com/llvm/llvm-project',
107
+ };
108
+ const llvmText = 'license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT';
109
+ if (platform === 'linux') {
110
+ return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the addon; ${llvmText}` }];
111
+ }
112
+ const mingwLicense = 'LicenseRef-MinGW-w64-runtime';
113
+ return [
114
+ { ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the addon; ${llvmText}` },
115
+ {
116
+ ...shared,
117
+ name: 'mingw-w64-runtime',
118
+ license: mingwLicense,
119
+ licenseDeclared: mingwLicense,
120
+ sourceUrl: 'https://github.com/mingw-w64/mingw-w64',
121
+ 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)',
122
+ },
123
+ ];
124
+ }
125
+
93
126
  async function buildRow(node) {
94
127
  const recipe = await loadJs(node.paths.project, 'crossbind.build');
95
128
  const manifest = node.package || null;
@@ -152,5 +185,6 @@ export default async function collectLicenseRows(platform = null) {
152
185
  if (platform) rows.push(...await bundledRowsOf(node, platform));
153
186
  }
154
187
  if (platform === 'wasi') rows.push(...wasiToolchainRows());
188
+ if (platform === 'linux' || platform === 'win32') rows.push(...addonToolchainRows(platform));
155
189
  return rows.sort((a, b) => a.name.localeCompare(b.name));
156
190
  }
@@ -6,6 +6,7 @@ import replaceBasePathForDockerUtil from '../utils/replaceBasePathForDocker.js';
6
6
  import { DOCKER_RUN_SECURITY_ARGS } from '../utils/dockerSecurity.js';
7
7
  import state from '../state/index.js';
8
8
  import { wasiCFlags, wasiCxxFlags, resolveWasiSdkPath, WASI_TARGET_TRIPLE } from '../utils/wasiToolchain.js';
9
+ import { HOST_BUILT_PLATFORMS } from '../utils/targets.js';
9
10
 
10
11
  // Native builds can outrun Node's 1 MiB default pipe buffer; without a raised cap a successful build dies with ENOBUFS.
11
12
  const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
@@ -64,6 +65,18 @@ const androidParamsX86_64 = [
64
65
 
65
66
  // Xcode 27 refuses deployment targets below 15.0; React Native's own floor is 15.1.
66
67
  const IOS_DEPLOYMENT_TARGET = '15.1';
68
+ // Node 22, the oldest supported line, needs macOS 11.
69
+ const DARWIN_DEPLOYMENT_TARGET = '11.0';
70
+ const DARWIN_HOST_ARCH = process.arch === 'x64' ? 'x86_64' : 'arm64';
71
+ const DARWIN_HOST_PACKAGE_PREFIXES = ['/opt/homebrew', '/usr/local', '/opt/local'];
72
+ // Apple's own tools only, which find the SDK through xcrun. A GNU ar earlier on the PATH (Homebrew's
73
+ // binutils) writes archives that Apple's linker rejects.
74
+ const DARWIN_BUILD_PATH = '/usr/bin:/bin:/usr/sbin:/sbin';
75
+ // The linux image's clang wrappers and CMake toolchain files, one per target triple.
76
+ const LINUX_TOOLCHAIN = '/opt/crossbind/linux';
77
+ // The windows image's llvm-mingw and its CMake toolchain files, one per target triple.
78
+ const LLVM_MINGW = '/opt/llvm-mingw';
79
+ const WINDOWS_TOOLCHAIN = '/opt/crossbind/windows';
67
80
  const IOS_HOST_FLAGS = `-arch arm64 -isysroot ${iosSdkPath} -fembed-bitcode`;
68
81
  const IOS_SIM_HOST_FLAGS = `-arch arm64 -isysroot ${iosSimSdkPath} -fembed-bitcode`;
69
82
  const IOS_IPHONE_PARAMS = ['-e', `CFLAGS="${IOS_HOST_FLAGS}"`, '-e', `CXXFLAGS="${IOS_HOST_FLAGS}"`, '-e', `LDFLAGS="${IOS_HOST_FLAGS}"`];
@@ -118,7 +131,7 @@ export default function run(program, params = [], platformPrefix = null, target
118
131
 
119
132
  // wasi is host-run only with a locally configured wasi-sdk; otherwise docker carries it.
120
133
  const wasiHostSdk = target?.platform === 'wasi' ? resolveWasiSdkPath(state.config.system) : null;
121
- if ((target?.platform !== 'ios' && !(target?.platform === 'wasi' && wasiHostSdk)) || program !== null) {
134
+ if ((!HOST_BUILT_PLATFORMS.includes(target?.platform) && !(target?.platform === 'wasi' && wasiHostSdk)) || program !== null) {
122
135
  // Google ships the linux NDK for x86_64 only, so the android index carries no arm64 leaf;
123
136
  // pull its amd64 leaf by ref or an arm64 host finds no matching manifest.
124
137
  pullDockerImage(imageRoleFor(target), target?.platform === 'android' ? 'linux/amd64' : undefined);
@@ -237,6 +250,71 @@ export default function run(program, params = [], platformPrefix = null, target
237
250
  }
238
251
  }
239
252
  break;
253
+ case 'linux': {
254
+ [dProgram, ...dParams] = params;
255
+ const triple = target.arch === 'x64' ? 'x86_64-linux-gnu' : 'aarch64-linux-gnu';
256
+ const tool = (name) => `${LINUX_TOOLCHAIN}/bin/${triple}-${name}`;
257
+ // The image's own .pc files describe its libraries, not the sysroot's. Every archive
258
+ // ends up inside a loadable .node module, so -fPIC holds even where a project turns
259
+ // position-independent code off for static builds, as GDAL does.
260
+ platformParams = [
261
+ '-e', `CC=${tool('clang')}`, '-e', `CXX=${tool('clang++')}`,
262
+ '-e', `AR=${tool('ar')}`, '-e', `RANLIB=${tool('ranlib')}`,
263
+ '-e', `NM=${tool('nm')}`, '-e', `STRIP=${tool('strip')}`,
264
+ '-e', 'PKG_CONFIG_LIBDIR=',
265
+ '-e', 'CFLAGS=-fPIC', '-e', 'CXXFLAGS=-fPIC',
266
+ ];
267
+ if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
268
+ dParams = [...dParams, `-DCMAKE_TOOLCHAIN_FILE=${LINUX_TOOLCHAIN}/${triple}.cmake`];
269
+ }
270
+ break;
271
+ }
272
+ case 'win32': {
273
+ [dProgram, ...dParams] = params;
274
+ const triple = target.arch === 'x64' ? 'x86_64-w64-mingw32' : 'aarch64-w64-mingw32';
275
+ const tool = (name) => `${LLVM_MINGW}/bin/${triple}-${name}`;
276
+ platformParams = [
277
+ '-e', `CC=${tool('clang')}`, '-e', `CXX=${tool('clang++')}`,
278
+ '-e', `AR=${tool('ar')}`, '-e', `RANLIB=${tool('ranlib')}`,
279
+ '-e', `NM=${tool('nm')}`, '-e', `STRIP=${tool('strip')}`,
280
+ '-e', `RC=${tool('windres')}`, '-e', `WINDRES=${tool('windres')}`,
281
+ '-e', 'PKG_CONFIG_LIBDIR=',
282
+ ];
283
+ if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
284
+ dParams = [...dParams, `-DCMAKE_TOOLCHAIN_FILE=${WINDOWS_TOOLCHAIN}/${triple}.cmake`];
285
+ }
286
+ break;
287
+ }
288
+ case 'darwin': {
289
+ [dProgram, ...dParams] = params;
290
+ const appleArch = target.arch === 'x64' ? 'x86_64' : 'arm64';
291
+ // Homebrew and MacPorts packages exist on the build machine only: an archive compiled
292
+ // against one fails to link, or to load, anywhere else. The SDK's libraries stay visible.
293
+ platformParams = ['-e', 'PKG_CONFIG_LIBDIR='];
294
+ if (dProgram === 'cmake') {
295
+ if (dParams[0] !== '--build' && dParams[0] !== '--install') {
296
+ dParams = [
297
+ ...dParams,
298
+ `-DCMAKE_OSX_ARCHITECTURES=${appleArch}`,
299
+ `-DCMAKE_OSX_DEPLOYMENT_TARGET=${DARWIN_DEPLOYMENT_TARGET}`,
300
+ // Every archive ends up inside a loadable .node module.
301
+ '-DCMAKE_POSITION_INDEPENDENT_CODE=ON',
302
+ `-DCMAKE_IGNORE_PREFIX_PATH=${DARWIN_HOST_PACKAGE_PREFIXES.join(';')}`,
303
+ // The other architecture's probes cannot run here, so CMake has to know it cross-compiles.
304
+ ...(appleArch === DARWIN_HOST_ARCH ? [] : ['-DCMAKE_SYSTEM_NAME=Darwin', `-DCMAKE_SYSTEM_PROCESSOR=${appleArch}`]),
305
+ ];
306
+ }
307
+ } else {
308
+ const flags = `-arch ${appleArch} -mmacosx-version-min=${DARWIN_DEPLOYMENT_TARGET}`;
309
+ // A --host triple sends configure after prefixed compilers (aarch64-apple-darwin-cc) that
310
+ // Xcode does not ship, and SQLite's autosetup has no fallback to the plain names.
311
+ platformParams.push(
312
+ '-e', 'CC=/usr/bin/clang', '-e', 'CXX=/usr/bin/clang++',
313
+ '-e', `CFLAGS=${flags}`, '-e', `CXXFLAGS=${flags}`, '-e', `LDFLAGS=${flags}`,
314
+ );
315
+ }
316
+ break;
317
+ }
240
318
  default:
241
319
  }
242
320
  }
@@ -244,7 +322,7 @@ export default function run(program, params = [], platformPrefix = null, target
244
322
  const env = {};
245
323
  let runner = 'DOCKER';
246
324
  if (
247
- ((target?.platform === 'ios' || (target?.platform === 'wasi' && wasiHostSdk)) && program === null) ||
325
+ ((HOST_BUILT_PLATFORMS.includes(target?.platform) || (target?.platform === 'wasi' && wasiHostSdk)) && program === null) ||
248
326
  state.config.system.RUNNER === 'LOCAL'
249
327
  ) {
250
328
  runner = 'LOCAL';
@@ -291,6 +369,12 @@ export default function run(program, params = [], platformPrefix = null, target
291
369
  let fileExecParams;
292
370
  if (runner === 'LOCAL') {
293
371
  env.PATH = `/opt/homebrew/bin:${env.PATH}`;
372
+ // A package's *-config script on the PATH (nc-config, sfcgal-config, ...) would compile that
373
+ // package into the archives. cmake is the one host tool a darwin build takes from the PATH.
374
+ if (target?.platform === 'darwin') {
375
+ if (dProgram === 'cmake') dProgram = findExecutable('cmake', env.PATH) ?? dProgram;
376
+ env.PATH = DARWIN_BUILD_PATH;
377
+ }
294
378
 
295
379
  const options = {
296
380
  cwd: dockerOptions.workdir || buildPath,
@@ -351,6 +435,17 @@ export default function run(program, params = [], platformPrefix = null, target
351
435
  }
352
436
  }
353
437
 
438
+ function findExecutable(name, searchPath) {
439
+ return searchPath.split(':').map((dir) => `${dir}/${name}`).find((file) => {
440
+ try {
441
+ fs.accessSync(file, fs.constants.X_OK);
442
+ return true;
443
+ } catch {
444
+ return false;
445
+ }
446
+ });
447
+ }
448
+
354
449
  function replaceBasePathForDocker(data) {
355
450
  return replaceBasePathForDockerUtil(data, state.config.paths.base);
356
451
  }
@@ -30,7 +30,7 @@ if(ANDROID)
30
30
  set(EXTRA_LINK_LIBRARIES "log")
31
31
  endif()
32
32
 
33
- if (NOT ${COMPILE_OPTIONS} STREQUAL "")
33
+ if (NOT "${COMPILE_OPTIONS}" STREQUAL "")
34
34
  target_compile_options("${PROJECT_NAME}" PUBLIC "${COMPILE_OPTIONS}")
35
35
  endif()
36
36
 
@@ -51,7 +51,7 @@ if(NOT PACKAGE_HOST_INDEX EQUAL -1)
51
51
  target_link_libraries("${PROJECT_NAME}" INTERFACE "${PROJECT_LIBS_DIR}")
52
52
  endif()
53
53
 
54
- if(NOT APPLE)
54
+ if(NOT (APPLE AND CMAKE_SYSTEM_NAME STREQUAL "iOS"))
55
55
  target_include_directories("${PROJECT_NAME}" INTERFACE "${PROJECT_SOURCE_DIR}/${PROJECT_TARGET_HOST}/include")
56
56
  endif()
57
57
  endif()
@@ -210,9 +210,13 @@ Comlink.transferHandlers.set('embindObject', {
210
210
  && typeof obj.isDeleted === 'function';
211
211
  },
212
212
  serialize(obj) {
213
- const id = registerEmbindObject(obj);
213
+ // An object a property returns comes wrapped by its owner's coercion proxy. Registering and exposing
214
+ // the raw object, as the proxy handler does, keeps objects read from it one wrapper deep, which an
215
+ // argument's unwrapping undoes.
216
+ const raw = unwrapCoercionProxy(obj);
217
+ const id = registerEmbindObject(raw);
214
218
  const { port1, port2 } = new MessageChannel();
215
- Comlink.expose(wrapWithVectorCoercion(obj), port1);
219
+ Comlink.expose(wrapWithVectorCoercion(raw), port1);
216
220
  return [{ __embindId: id, port: port2 }, [port2]];
217
221
  },
218
222
  deserialize(data) {
@@ -1,35 +1,49 @@
1
1
  {
2
- "version": "1.0.6",
2
+ "version": "1.0.8",
3
3
  "registry": "ghcr.io/crossbind",
4
4
  "toolchains": {
5
5
  "rust": "1.98.1"
6
6
  },
7
7
  "images": {
8
8
  "rust-sysroot": {
9
- "index": "sha256:b82285c6c74f0f2f21bd4f3d07af8a005b16b9e8faaecbe672f6e9b139616eb7",
9
+ "index": "sha256:d816d27f1f3b110aed92f7f81cb8eb99f58ba95cafc96a797c46f30536549a45",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:50f2bc41d084157bb2d63caa8a91486ea15d3aff248e0d84013a20c7b8cf0c57",
12
- "linux/amd64": "sha256:65c63d0b3df31406c514a6ed9dbb5ba0940b27c94e2e89b3ceafedaee339bca8"
11
+ "linux/arm64": "sha256:217f908b3e1c883bed8cd4db65aae48044481e88126c09c3ce9bf754288d8720",
12
+ "linux/amd64": "sha256:15719494b51ea8ee6f3a71556b2c04f37828e632bc46d2c3fb7121a1afb37303"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:3a742a779a68e0180ca43232b9e28047e469462c575f1cdef8a3af3ba984263b",
16
+ "index": "sha256:051aa70184d29e95d5192cc60cc42523b42fab72291efbf13f71006482f063c0",
17
17
  "platforms": {
18
- "linux/amd64": "sha256:ff2a42fe5cdff75a9ce89fde340dbde5655f639de2e1288baeb61f2e82d1d095",
19
- "linux/arm64": "sha256:2571179e6597369544a887da197e280ad9d15075719c04113cc2fd243358f56d"
18
+ "linux/arm64": "sha256:cdf189ad4ae220698088fda54c4a5005a207542bba8ab2c01fe58fc008586df8",
19
+ "linux/amd64": "sha256:f501545972e2a253b19f1900946cfe39ce5dea74600d885cae47db3f320b506b"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:cd9065909b8e4aea584fbe6f6e0215773feaed97ab46e5ce265a487171f87757",
23
+ "index": "sha256:89e0ea288d42d9f24671e43b3e59dc6b9913e389b68e51d8f0e314e81d111bf3",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:224665ea653e6ff6e483fc24b226fa68bf45173a2598439665d827780cbe5534",
26
- "linux/amd64": "sha256:862b197c13dbdf163d480d35e8b17f96b23b447701cb8e79f0252773d7a48a42"
25
+ "linux/amd64": "sha256:40fe9f7573ee5a696c68043b12486e4562e0b2a87f74ef21a9b11c08454aff75",
26
+ "linux/arm64": "sha256:7172f994c262600dcd1334806eee9b44e873ac42fe286bf8b0983491586d1225"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:0d0b56a76cff5b16f52eba3a4d2b17bc888fb2ca502bfd0ae029bf837c6c354d",
30
+ "index": "sha256:ef6dc5ed724e7ea977a1f644793967a5ad7ff62b9a6bb6f2971d4a2a9e8f5a4b",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:77fb9007369c5c8003024aade231642afb798664f82aada1a8ac26b931a15fbf"
32
+ "linux/amd64": "sha256:d6843be98e4bc56a21e39b2670d081f60d27b45c149c0ee132d2c45bde337ec8"
33
+ }
34
+ },
35
+ "linux": {
36
+ "index": "sha256:e8b6e6afd1af9f3a84eee739bd25ce6abb8416c5af87a175b780cd18d871513d",
37
+ "platforms": {
38
+ "linux/amd64": "sha256:39fe90d1cb2fbb411823bc396d463e298020b5a93b9a5244f27644c5c0b87b2a",
39
+ "linux/arm64": "sha256:9cd9550a39010bf7f00bf08d8505a22f85e31155bf63c8a46597852dc72ef4cc"
40
+ }
41
+ },
42
+ "windows": {
43
+ "index": "sha256:c90a7c495e8becc6154b20fc6226362813938413bb3af626d0d0982cceaf32c5",
44
+ "platforms": {
45
+ "linux/amd64": "sha256:1fbc5c59e09253cfe86bb54fa76f0c1c6b1c1e942e4059725658340f64708e59",
46
+ "linux/arm64": "sha256:229389bbabe838124cbb572b91b42ae0e3fdcd6020462203fdbbcf5864c4a234"
33
47
  }
34
48
  }
35
49
  }