crossbind 2.0.0-beta.60 → 2.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +8 -6
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +37 -9
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildJs.js +1 -1
  6. package/src/actions/buildLib.js +27 -8
  7. package/src/actions/buildNativeCommand.js +84 -0
  8. package/src/actions/buildNode.js +190 -0
  9. package/src/actions/buildTypes.js +8 -2
  10. package/src/actions/buildWasiCommand.js +0 -4
  11. package/src/actions/buildWasm.js +92 -95
  12. package/src/actions/cloudCommands.js +133 -0
  13. package/src/actions/createImportedBridges.js +64 -0
  14. package/src/actions/createInterface.js +318 -65
  15. package/src/actions/createLib.js +60 -43
  16. package/src/actions/getAllBridges.js +1 -1
  17. package/src/actions/getData.js +4 -1
  18. package/src/actions/getLinkInputs.js +70 -0
  19. package/src/actions/isSourceNewer.js +2 -0
  20. package/src/actions/licenses.js +175 -17
  21. package/src/actions/prepareConanDependencies.js +51 -0
  22. package/src/actions/publishBinary.js +32 -0
  23. package/src/actions/run.js +124 -32
  24. package/src/actions/runnerCommands.js +421 -0
  25. package/src/actions/writeRuntimeEntry.js +69 -0
  26. package/src/assets/cmake/CMakeLists.txt +1 -1
  27. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  28. package/src/assets/cmake/command/command.cpp +1 -0
  29. package/src/assets/cmake/dist.cmake +2 -1
  30. package/src/assets/configure/install.sh +22 -0
  31. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  32. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  33. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
  34. package/src/assets/js-runtime/core.js +11 -0
  35. package/src/assets/node-import-hooks.mjs +61 -0
  36. package/src/assets/toolchain-digests.json +27 -13
  37. package/src/assets/wasi-runtime/stubs.c +29 -0
  38. package/src/bin.js +290 -37
  39. package/src/index.js +6 -1
  40. package/src/integration/bindHeaderImports.js +18 -0
  41. package/src/integration/getCrossbindScript.js +53 -19
  42. package/src/integration/getDependFilePath.js +18 -12
  43. package/src/integration/resolveNativeImport.js +14 -0
  44. package/src/node/dev.mjs +19 -0
  45. package/src/runner/blobs.js +135 -0
  46. package/src/runner/files.js +193 -0
  47. package/src/runner/server.js +320 -0
  48. package/src/state/attachConanDependencies.js +56 -0
  49. package/src/state/index.js +31 -20
  50. package/src/state/loadConfig.js +36 -24
  51. package/src/state/refreshBuiltDependencies.js +20 -0
  52. package/src/state/refreshConanDependencies.js +17 -0
  53. package/src/utils/androidToolchain.js +4 -0
  54. package/src/utils/appRustCrates.js +17 -1
  55. package/src/utils/appSources.js +112 -0
  56. package/src/utils/archiveFlags.js +12 -0
  57. package/src/utils/boundHeaders.js +79 -0
  58. package/src/utils/bridgeExtras.js +486 -0
  59. package/src/utils/bridgeRuntimeFixes.js +48 -0
  60. package/src/utils/bundledLicenses.js +26 -0
  61. package/src/utils/cargoImport.js +44 -0
  62. package/src/utils/cargoTarget.js +23 -3
  63. package/src/utils/cloudAccount.js +55 -0
  64. package/src/utils/conanDependencies.js +66 -0
  65. package/src/utils/conanImport.js +58 -0
  66. package/src/utils/conanInstall.js +154 -0
  67. package/src/utils/conanProfile.js +203 -0
  68. package/src/utils/conanStage.js +292 -0
  69. package/src/utils/copyToDistSource.js +13 -0
  70. package/src/utils/cppDts.js +134 -55
  71. package/src/utils/darwinToolchain.js +11 -0
  72. package/src/utils/dirLock.js +137 -28
  73. package/src/utils/downloadAndExtractFile.js +34 -7
  74. package/src/utils/embindArgumentGuards.js +84 -0
  75. package/src/utils/embindCallArguments.js +19 -0
  76. package/src/utils/embindJspiQueue.js +25 -0
  77. package/src/utils/embindRsFingerprint.js +26 -0
  78. package/src/utils/execContainer.js +49 -0
  79. package/src/utils/headerImports.js +56 -0
  80. package/src/utils/hostPlatform.js +21 -0
  81. package/src/utils/inputStamp.js +23 -0
  82. package/src/utils/iosToolchain.js +6 -0
  83. package/src/utils/isSourceCmakePackage.js +10 -0
  84. package/src/utils/licenseReport.js +11 -2
  85. package/src/utils/linkLayout.js +8 -1
  86. package/src/utils/linuxToolchain.js +24 -0
  87. package/src/utils/makeTreeWritable.js +12 -0
  88. package/src/utils/nativeImports.js +23 -0
  89. package/src/utils/nativeSourceStamp.js +36 -0
  90. package/src/utils/nodeDevBuild.js +92 -0
  91. package/src/utils/packageLicense.js +101 -0
  92. package/src/utils/pthreadMainScript.js +14 -0
  93. package/src/utils/pullDockerImage.js +10 -5
  94. package/src/utils/relocatePrebuilt.js +119 -0
  95. package/src/utils/remoteClient.js +127 -0
  96. package/src/utils/remoteOutputs.js +39 -0
  97. package/src/utils/remoteRunner.js +97 -0
  98. package/src/utils/remoteUpload.js +134 -0
  99. package/src/utils/replaceFile.js +8 -0
  100. package/src/utils/resolveCompanionPackage.js +51 -0
  101. package/src/utils/resolveEmbindNapi.js +22 -0
  102. package/src/utils/resolveEmbindRust.js +17 -52
  103. package/src/utils/runCargo.js +51 -53
  104. package/src/utils/runConan.js +257 -0
  105. package/src/utils/runtimeEntries.js +56 -0
  106. package/src/utils/rustBridgeGen.js +903 -253
  107. package/src/utils/rustMt.js +4 -0
  108. package/src/utils/scopedEmbind.js +22 -0
  109. package/src/utils/selectRunner.js +81 -0
  110. package/src/utils/sourceFingerprint.js +4 -3
  111. package/src/utils/swigInterface.js +86 -6
  112. package/src/utils/systemKeys.js +43 -3
  113. package/src/utils/targets.js +104 -3
  114. package/src/utils/toolchainNotices.js +5 -0
  115. package/src/utils/wasiToolchain.js +14 -1
  116. package/src/utils/windowsToolchain.js +18 -0
  117. package/vitest.config.js +11 -0
  118. package/src/utils/cppFieldBindings.js +0 -23
@@ -2,23 +2,59 @@
2
2
  import fs from 'node:fs';
3
3
  import upath from 'upath';
4
4
  import state, { saveCache } from '../state/index.js';
5
- import { getFileHash } from '../utils/hash.js';
5
+ import refreshConanDependencies from '../state/refreshConanDependencies.js';
6
+ import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
7
+ import { conanInputsOf } from '../utils/conanDependencies.js';
8
+ import { getContentHash, getFileHash } from '../utils/hash.js';
6
9
  import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
7
- import { writeHeaderDts, parseCppSurface } from '../utils/cppDts.js';
8
- import { injectFieldBindings } from '../utils/cppFieldBindings.js';
10
+ import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
9
11
  import {
10
- buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, indexTypeDefinitions, interfaceIncludes,
11
- interfaceToRetryWithoutMacros, parseMacroDump, referencedTypeHeaders, selectSwigMacros,
12
+ interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
13
+ } from '../utils/bridgeExtras.js';
14
+ import resolveNativeImport from '../integration/resolveNativeImport.js';
15
+ import { writeHeaderDts, writeConanImportDts } from '../utils/cppDts.js';
16
+ import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
17
+ import {
18
+ buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
19
+ indexTypeDefinitions, inlineIncludes, interfaceIncludes, interfaceToRetryWithoutMacros, interfaceWithAllFunctions, parseMacroDump, referencedTypeHeaders,
20
+ selectSwigMacros,
12
21
  } from '../utils/swigInterface.js';
13
22
  import writeIfChanged from '../utils/writeIfChanged.js';
23
+ import { withDirLockSync } from '../utils/dirLock.js';
24
+ import { imageRoleFor } from '../utils/pullDockerImage.js';
25
+ import { conanImportOfHeader } from '../utils/conanImport.js';
14
26
  import run, { cxxPreprocessorFor } from './run.js';
27
+ import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
15
28
 
16
29
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
17
- const INTERFACE_FORMAT = 'swig-macros-1';
30
+ const INTERFACE_FORMAT = 'swig-macros-3';
31
+ // Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
32
+ // bridges from a fork before field bindings and constants are rebuilt.
33
+ const BRIDGE_FORMAT = 'swig-extras-1';
18
34
  const predefinedMacros = new Map();
19
35
  const typeDefinitions = new Map();
20
36
 
21
- export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm'), { withDependencies = true } = {}) {
37
+ // Each process keeps the cache it loaded, so an interface or bridge another one has rebuilt since must not pass for
38
+ // this one's: beside each file is the hash of what it was made from, blank while the file is written. A file without one
39
+ // comes from an older crossbind or a cache CI restores, and its cache record holds.
40
+ const keyOf = (file) => `${file}.key`;
41
+ const isMadeFrom = (file, hash) => !fs.existsSync(keyOf(file)) || fs.readFileSync(keyOf(file), 'utf8') === getContentHash(hash);
42
+
43
+ // Metro binds headers in worker processes, so two of them can generate one bridge at once. The headers a header's types
44
+ // come from are bound in its turn.
45
+ export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm'), options = {}) {
46
+ if (options.withDependencies === false) return bindHeader(headerOrModuleFilePath, target, options);
47
+ return withDirLockSync(`${state.config.paths.build}/bridge.lock`, () => bindHeader(headerOrModuleFilePath, target, options), {
48
+ pollMs: 50,
49
+ heldBy: 'another process is binding a header',
50
+ });
51
+ }
52
+
53
+ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, wholeHeaders = [] } = {}) {
54
+ // A header can include a Conan package's headers that a build staged after this process loaded, or belong to a
55
+ // dependency built since.
56
+ refreshConanDependencies(state.config);
57
+ refreshBuiltDependencies(state.config);
22
58
  const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
23
59
  if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
24
60
  fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
@@ -29,33 +65,42 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
29
65
  // The header may live outside paths.native (e.g. a package-subpath import like
30
66
  // '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
31
67
  const sourceDir = upath.dirname(interfaceFilePath);
32
- const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir);
33
- const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, getFileHash(interfaceFilePath));
68
+ const allNames = wholeHeaders.some((file) => upath.resolve(file) === interfaceFilePath);
69
+ const names = bindingNames(interfaceFilePath, target, allNames);
70
+ if (withDependencies) warnUnimported(interfaceFilePath, names.functions);
71
+ const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir, names);
72
+ const { headerPath } = includeLocation(interfaceFilePath, target);
73
+ const inlined = findSwigInlineIncludes(interfaceFilePath, [state.config, ...state.config.allDependencies], headerPath);
74
+ const swigView = inlined.length ? writeSwigView(interfaceFilePath, headerPath, inlined) : null;
75
+ const sourceHash = [getFileHash(interfaceFilePath), ...conanInputsHash()].join('\n');
76
+ // The names imported by name decide what the bridge adds to SWIG's output, so another one makes another bridge.
77
+ const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
78
+ const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
79
+ const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, bridgeSourceHash, swigView, extras);
34
80
  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
81
  if (bridgeFile) {
43
82
  // Records which header this bridge came from, so directory-driven consumers
44
83
  // (getAllBridges) can prune bridges whose header no longer exists.
45
84
  writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
46
- writeHeaderDts({
85
+ const dtsOptions = {
47
86
  headerFile: interfaceFilePath,
48
87
  exportsFile: `${bridgeFile}.exports.json`,
49
88
  projectPath: state.config.paths.project,
50
89
  cacheDir: state.config.paths.cache,
51
90
  dtsMode: state.config.dts,
52
- });
91
+ };
92
+ writeHeaderDts(dtsOptions);
93
+ if (conanImportOfHeader(interfaceFilePath, state.config.paths.cache)) {
94
+ dtsOptions.declarationsFile = createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
95
+ }
96
+ writeConanImportDts(dtsOptions);
53
97
  }
54
98
  if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
55
99
  // A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
100
+ // Such a bridge replaces the header's own, so a header bound whole keeps every constant here too.
56
101
  const dependencyBridges = dependencyHeadersOf(interfaceFilePath, target).map((header) => {
57
102
  try {
58
- return createBridgeFile(header, target, { withDependencies: false });
103
+ return createBridgeFile(header, target, { withDependencies: false, wholeHeaders });
59
104
  } catch (e) {
60
105
  console.warn(`crossbind: ${upath.basename(interfaceFilePath)} uses types from ${upath.basename(header)}, whose bindings failed (${e.message})`);
61
106
  return null;
@@ -66,7 +111,36 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
66
111
  return bridgeFile;
67
112
  }
68
113
 
69
- function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
114
+ // The Conan packages a header's includes may reach, for the interface and bridge caches: they are
115
+ // staged after a header may already be bound, and restaged in place for another version.
116
+ function conanInputsHash() {
117
+ const conan = conanInputsOf(state.config);
118
+ return conan ? [`conan:${getContentHash(JSON.stringify(conan))}`] : [];
119
+ }
120
+
121
+ // A header binds the constants the app imports from it, and so the free functions of a dependency's header: the project's
122
+ // own headers and .i modules bind every function.
123
+ function bindingNames(headerOrModuleFilePath, target, allNames) {
124
+ if (new RegExp(`.(${state.config.ext.module.join('|')})$`).test(headerOrModuleFilePath)) return { constants: [], functions: ALL_NAMES, named: [] };
125
+ const { names, takesModule, named } = headerUse(headerOrModuleFilePath, target);
126
+ if (allNames) return { constants: ALL_NAMES, functions: ALL_NAMES, named };
127
+ // A package's own .i says what the header binds.
128
+ const shipsInterface = fs.existsSync(headerOrModuleFilePath.replace(/\.[^./]+$/, '.i'));
129
+ const bindsEveryFunction = takesModule || shipsInterface || !includeLocation(headerOrModuleFilePath, target).isDependency;
130
+ return { constants: names, functions: bindsEveryFunction ? ALL_NAMES : names, named };
131
+ }
132
+
133
+ const warnedUnimported = new Set();
134
+
135
+ // A bundler transforms the header because something imports it, but no source the scan reads names it: a package in
136
+ // node_modules, say.
137
+ function warnUnimported(header, functions) {
138
+ if (functions === ALL_NAMES || functions.length || warnedUnimported.has(header)) return;
139
+ warnedUnimported.add(header);
140
+ console.warn(`crossbind: ${upath.basename(header)} binds none of its functions, since no source of the app imports a name from it. Import the functions you call by name, or the whole header with import * as.`);
141
+ }
142
+
143
+ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { constants, functions, named = [] }) {
70
144
  if (!headerOrModuleFilePath) {
71
145
  return null;
72
146
  }
@@ -76,62 +150,87 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
76
150
  const packages = [state.config, ...state.config.allDependencies];
77
151
  const prelude = isModule ? [] : findHeaderPrelude(headerOrModuleFilePath, packages, headerPath);
78
152
  const ignored = isModule ? [] : findIgnoredDeclarations(headerOrModuleFilePath, packages, headerPath);
153
+ const preamble = isModule ? [] : findSwigPreamble(headerOrModuleFilePath, packages, headerPath);
79
154
  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.
155
+ const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
156
+ const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
157
+ // A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
158
+ // interface the package ships beside the header changing or going away, another imported name, or Conan packages
159
+ // its includes may reach being staged or moving to another version must regenerate the interface as well.
82
160
  const fileHash = [
83
161
  INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
84
162
  ...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
163
+ ...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
164
+ ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
165
+ ...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
166
+ ...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
167
+ ...(named.length ? [`named:${named.join(',')}`] : []),
168
+ ...conanInputsHash(),
85
169
  ].join('\n');
86
170
  const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
87
- if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)) {
171
+ if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
172
+ && isMadeFrom(cachedInterface, fileHash) && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
88
173
  return cachedInterface;
89
174
  }
90
175
 
91
176
  if (isModule) {
92
177
  const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
93
- fs.copyFileSync(headerOrModuleFilePath, newPath);
94
- state.cache.interfaces[headerOrModuleFilePath] = newPath;
95
- state.cache.hashes[headerOrModuleFilePath] = fileHash;
96
- saveCache();
97
- return newPath;
178
+ return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
98
179
  }
99
180
 
100
- const temp = headerOrModuleFilePath.match(/^(.*)\..+?$/);
101
- if (!temp || temp.length < 2) return null;
102
-
103
- const filePathWithoutExt = temp[1];
104
- const interfaceFile = `${filePathWithoutExt}.i`;
181
+ if (!filePathWithoutExt) return null;
105
182
 
106
183
  if (fs.existsSync(interfaceFile)) {
107
184
  const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
108
- fs.copyFileSync(interfaceFile, newPath);
109
- state.cache.interfaces[headerOrModuleFilePath] = newPath;
110
- state.cache.hashes[headerOrModuleFilePath] = fileHash;
111
- saveCache();
112
- return newPath;
185
+ return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
113
186
  }
114
187
 
115
- const fileName = filePathWithoutExt.split('/').at(-1);
188
+ const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
116
189
 
190
+ const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
191
+ const extras = interfaceExtras(named, macros.table);
117
192
  const content = buildInterfaceContent({
118
193
  moduleName: fileName.toUpperCase(),
119
194
  headerPath,
120
195
  prelude,
121
196
  completing,
122
- swigMacros: collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir),
197
+ swigMacros: macros.lines,
123
198
  ignored,
199
+ constants,
200
+ preamble,
201
+ functions: functions === ALL_NAMES ? ALL_NAMES : [...functions, ...extras.functions],
202
+ extras: extras.lines,
124
203
  });
125
204
  const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
126
- fs.writeFileSync(outputFilePath, content);
205
+ return recordInterface(headerOrModuleFilePath, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
206
+ }
127
207
 
128
- state.cache.interfaces[headerOrModuleFilePath] = outputFilePath;
129
- state.cache.hashes[headerOrModuleFilePath] = fileHash;
208
+ // The key is blank while the file is written, so a process that dies in between leaves no file passing for another hash.
209
+ function recordInterface(headerFile, interfaceFile, fileHash, write) {
210
+ fs.writeFileSync(keyOf(interfaceFile), '');
211
+ write();
212
+ fs.writeFileSync(keyOf(interfaceFile), getContentHash(fileHash));
213
+ state.cache.interfaces[headerFile] = interfaceFile;
214
+ state.cache.hashes[headerFile] = fileHash;
130
215
  saveCache();
216
+ return interfaceFile;
217
+ }
218
+
219
+ const interfacePath = (name) => `${state.config.paths.build}/interface/${name}.i`;
220
+ // Each platform build of a package ships its headers under dist/prebuilt/<target>/include: one header for the interface.
221
+ const headerIdentity = (file) => file.match(/\/dist\/prebuilt\/[^/]+\/include\/(.+)$/)?.[1] ?? file;
222
+ const sharedInterface = (headerFile, interfaceFile) => Object.entries(state.cache.interfaces)
223
+ .some(([other, file]) => headerIdentity(other) !== headerIdentity(headerFile) && file === interfaceFile);
131
224
 
132
- return outputFilePath;
225
+ // An interface is named after its header's file name. A second header with that name (tiffio.h and tiffio.hxx, or one
226
+ // name in two directories) takes its extension, then a hash of its path, so neither replaces the other's bridge.
227
+ function interfaceName(headerFile, base) {
228
+ const candidates = [base, `${base}_${upath.extname(headerFile).slice(1)}`, `${base}_${getContentHash(headerFile).slice(0, 8)}`];
229
+ return candidates.find((name) => !sharedInterface(headerFile, interfacePath(name))) ?? candidates.at(-1);
133
230
  }
134
231
 
232
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
233
+
135
234
  // A header is included by its path under a project header directory or a dependency's include directory; one found only
136
235
  // through its own directory has no include root to search.
137
236
  function includeLocation(headerFile, target) {
@@ -139,11 +238,12 @@ function includeLocation(headerFile, target) {
139
238
  if (projectRoot) return { includeRoot: projectRoot, headerPath: headerFile.substr(projectRoot.length + 1) };
140
239
  const dependencyRoots = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
141
240
  .filter((d) => d.startsWith(state.config.paths.base));
142
- const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${p}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
143
- return match ? { includeRoot: match[1], headerPath: match[2] } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
241
+ const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${escapeRegExp(p)}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
242
+ return match ? { includeRoot: match[1], headerPath: match[2], isDependency: true } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
144
243
  }
145
244
 
146
- // The headers under an include root are indexed once per build.
245
+ // The headers under an include root are indexed once per build. A copy of a header is that header, and the copy nearest
246
+ // the root stands for it (libpng installs its headers in include/ and include/libpng16/).
147
247
  function definitionsUnder(includeRoot) {
148
248
  if (!typeDefinitions.has(includeRoot)) {
149
249
  const extensions = new RegExp(`\\.(${state.config.ext.header.join('|')})$`, 'i');
@@ -152,48 +252,120 @@ function definitionsUnder(includeRoot) {
152
252
  .map((entry) => upath.relative(includeRoot, upath.join(entry.parentPath, entry.name)))
153
253
  .sort()
154
254
  .map((path) => ({ path, text: fs.readFileSync(upath.join(includeRoot, path), 'utf8') }));
155
- typeDefinitions.set(includeRoot, indexTypeDefinitions(files));
255
+ const depth = (path) => path.split('/').length;
256
+ const nearest = new Map();
257
+ files.forEach(({ path, text }) => {
258
+ if (!nearest.has(text) || depth(path) < depth(nearest.get(text))) nearest.set(text, path);
259
+ });
260
+ typeDefinitions.set(includeRoot, {
261
+ definitions: indexTypeDefinitions(files.filter(({ path, text }) => nearest.get(text) === path)),
262
+ copyOf: new Map(files.map(({ path, text }) => [path, nearest.get(text)])),
263
+ });
156
264
  }
157
265
  return typeDefinitions.get(includeRoot);
158
266
  }
159
267
 
268
+ function lookupContext(includeRoot, headerPath, headerText) {
269
+ const { definitions, copyOf } = definitionsUnder(includeRoot);
270
+ return { headerText, headerPath: copyOf.get(headerPath) ?? headerPath, definitions };
271
+ }
272
+
160
273
  function findCompletingIncludes(headerFile, includeRoot, headerPath) {
161
274
  const headerText = fs.readFileSync(headerFile, 'utf8');
162
275
  if (!headerText.includes('unique_ptr')) return [];
163
- return completingIncludes({ headerText, headerPath, definitions: definitionsUnder(includeRoot) });
276
+ return completingIncludes(lookupContext(includeRoot, headerPath, headerText));
164
277
  }
165
278
 
166
279
  function dependencyHeadersOf(headerFile, target) {
167
280
  const { includeRoot, headerPath } = includeLocation(headerFile, target);
168
281
  if (!includeRoot) return [];
169
282
  const headerText = fs.readFileSync(headerFile, 'utf8');
170
- return referencedTypeHeaders({ headerText, headerPath, definitions: definitionsUnder(includeRoot) })
283
+ return referencedTypeHeaders(lookupContext(includeRoot, headerPath, headerText))
171
284
  .map((header) => upath.join(includeRoot, header));
172
285
  }
173
286
 
287
+ // The proxy module's own exports, never names of the header.
288
+ const PROXY_NAMES = new Set(['initNative', 'AllSymbols']);
289
+ const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
290
+
291
+ // A package header resolves under the target's prebuilt directory, and the bundler may resolve it for another target.
292
+ const targetNeutral = (file) => file?.replace(/\/dist\/prebuilt\/[^/]+\/include\//, '/dist/prebuilt/*/include/');
293
+
294
+ function realPath(file) {
295
+ if (!file) return null;
296
+ try {
297
+ return upath.normalize(fs.realpathSync(file));
298
+ } catch (e) {
299
+ return upath.resolve(file);
300
+ }
301
+ }
302
+
303
+ // The names the app's own sources import from this header, or every name for `import * as` and the other whole-module
304
+ // imports. The app also takes the module, and reaches its functions by name, when it imports AllSymbols or when its only
305
+ // imports of the header take initNative alone.
306
+ // `named` keeps the names imported by name even when another import takes the whole module: macros, variadic functions
307
+ // and mutable globals bind only for those.
308
+ function headerUse(headerFile, target) {
309
+ const projectDir = state.config.paths.project;
310
+ if (!projectDir || !fs.existsSync(projectDir)) return { names: [], takesModule: false, named: [] };
311
+ const header = targetNeutral(realPath(headerFile));
312
+ const names = new Set();
313
+ let isImported = false;
314
+ let takesAllSymbols = false;
315
+ let takesWhole = false;
316
+ for (const { importer, specifier, names: imported } of findHeaderImportsIn(projectDir, state.config.ext.header)) {
317
+ if (targetNeutral(realPath(resolveNativeImport(specifier, importer, target))) !== header) continue;
318
+ if (imported === ALL_NAMES) {
319
+ takesWhole = true;
320
+ continue;
321
+ }
322
+ isImported = true;
323
+ const listed = imported.filter((name) => C_IDENTIFIER.test(name));
324
+ takesAllSymbols ||= listed.includes('AllSymbols');
325
+ listed.filter((name) => !PROXY_NAMES.has(name)).forEach((name) => names.add(name));
326
+ }
327
+ const named = [...names].sort();
328
+ if (takesWhole) return { names: ALL_NAMES, takesModule: true, named };
329
+ return { names: named, takesModule: takesAllSymbols || (isImported && names.size === 0), named };
330
+ }
331
+
174
332
  // 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) {
333
+ function collectSwigMacros(headerFile, includes, name, target, sourceDir, constants) {
176
334
  const interfaceDir = `${state.config.paths.build}/interface`;
177
335
  try {
178
336
  if (!predefinedMacros.has(target.path)) {
179
337
  predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
180
338
  }
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) });
339
+ const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target, [RELEASE_DEFINE]);
340
+ return {
341
+ lines: selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants }),
342
+ table: macros,
343
+ };
183
344
  } catch (e) {
184
345
  console.warn(`crossbind: SWIG reads ${upath.basename(headerFile)} without the macros of its includes (${e.message})`);
185
- return [];
346
+ return { lines: [], table: new Map() };
186
347
  }
187
348
  }
188
349
 
189
- function dumpMacros(outputFile, includes, includePath, target) {
190
- run(cxxPreprocessorFor(target), [
191
- '-x', 'c++', '-std=c++17', '-dM', '-E',
350
+ // Bridges compile in release with NDEBUG defined, so SWIG reads headers the same way: a declaration that exists only
351
+ // without NDEBUG (sqlite3_mutex_held) would otherwise be bound and fail to compile. The predefined dump stays without
352
+ // it, so NDEBUG counts as a macro SWIG needs rather than a compiler predefine.
353
+ const RELEASE_DEFINE = '-DNDEBUG';
354
+
355
+ // One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
356
+ // which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
357
+ // still come from the target's own packages.
358
+ const toolTargetFor = (target) => (imageRoleFor(target) === 'android' ? target : { ...target, platform: 'wasm' });
359
+
360
+ function dumpMacros(outputFile, includes, includePath, target, defines = []) {
361
+ const tool = toolTargetFor(target);
362
+ run(cxxPreprocessorFor(tool), [
363
+ '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
192
364
  ...includePath,
193
365
  ...includes.flatMap((header) => ['-include', header]),
194
366
  '-o', outputFile,
195
367
  '/dev/null',
196
- ], null, target);
368
+ ], null, tool);
197
369
  return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
198
370
  }
199
371
 
@@ -204,6 +376,9 @@ function swigIncludePath(target, sourceDir) {
204
376
  ...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
205
377
  ...state.config.paths.header,
206
378
  ...allHeaders,
379
+ // A package that ships its sources has no prebuilt include directory: the app reaches its headers through
380
+ // the package's own CMakeLists.
381
+ ...state.config.allDependencies.filter(isSourceCmakePackage).flatMap((d) => d.paths.header),
207
382
  ...(sourceDir ? [sourceDir] : []),
208
383
  ].filter((path) => !!path.toString()).map((path) => `-I${path}`);
209
384
  return [...new Set(includePath)];
@@ -215,37 +390,94 @@ function swigIncludePath(target, sourceDir) {
215
390
  // next time they are picked up.
216
391
  function applyAsyncGuard(bridgeFilePath) {
217
392
  const bridgeText = fs.readFileSync(bridgeFilePath, { encoding: 'utf8' });
218
- const guarded = guardAsyncBindings(bridgeText);
393
+ const guarded = guardAsyncBindings(fixBridgeRuntime(bridgeText));
219
394
  if (guarded !== bridgeText) {
220
395
  fs.writeFileSync(bridgeFilePath, guarded);
221
396
  }
222
397
  }
223
398
 
399
+ // A build hides SWIG's output, so the SWIG fork writes each binding it skipped beside the bridge, under the path SWIG saw.
400
+ function reportSkippedBindings(bridgeFilePath) {
401
+ const warningsFile = `${bridgeFilePath}.warnings`;
402
+ if (!fs.existsSync(warningsFile)) return;
403
+ fs.readFileSync(warningsFile, 'utf8').split('\n').filter(Boolean).forEach((line) => {
404
+ const [, file, lineNumber, message] = line.match(/^(.*):(\d+): (.*)$/) ?? [];
405
+ console.warn(`crossbind: ${file ? `${upath.basename(file)}:${lineNumber}: ${message}` : line}`);
406
+ });
407
+ }
408
+
409
+ // SWIG skips function-like macros, variadic functions and mutable globals: the ones the app imports bind here, after its
410
+ // output. Their C++ uses SWIG's pointer runtime, which SWIG writes only for a pointer binding, so a bridge without one
411
+ // is generated again with a binding that has one.
412
+ function addBridgeExtras(bridgeFilePath, interfaceFilePath, named, headerText, swig) {
413
+ const warningsFile = `${bridgeFilePath}.warnings`;
414
+ const readWarnings = () => (fs.existsSync(warningsFile) ? fs.readFileSync(warningsFile, 'utf8').split('\n').filter(Boolean) : []);
415
+ const interfaceText = fs.readFileSync(interfaceFilePath, 'utf8');
416
+ const module = interfaceText.match(/^%module (\w+)$/m)?.[1];
417
+ let extras = bridgeExtras({ interfaceText, named, bridgeText: fs.readFileSync(bridgeFilePath, 'utf8'), warnings: readWarnings(), headerText, module });
418
+ if (!extras.code && !extras.notes.length) return;
419
+ if (extras.code && !hasPointerRuntime(fs.readFileSync(bridgeFilePath, 'utf8'))) {
420
+ const anchored = withPointerRuntimeAnchor(interfaceText);
421
+ if (anchored !== interfaceText) {
422
+ fs.writeFileSync(interfaceFilePath, anchored);
423
+ swig();
424
+ applyAsyncGuard(bridgeFilePath);
425
+ }
426
+ if (!hasPointerRuntime(fs.readFileSync(bridgeFilePath, 'utf8'))) {
427
+ extras = {
428
+ code: '', exports: [], answered: new Set(),
429
+ notes: [...extras.notes, `${extras.exports.join(', ')}: these need the pointer runtime, which SWIG left out of this bridge; skipped.`],
430
+ };
431
+ }
432
+ }
433
+ if (extras.code) fs.appendFileSync(bridgeFilePath, `\n${extras.code}`);
434
+ const exportsFile = `${bridgeFilePath}.exports.json`;
435
+ const exported = JSON.parse(fs.readFileSync(exportsFile, 'utf8')).filter((name) => name !== POINTER_RUNTIME_ANCHOR);
436
+ fs.writeFileSync(exportsFile, `${JSON.stringify([...new Set([...exported, ...extras.exports])])}\n`);
437
+ fs.writeFileSync(warningsFile, [...readWarnings().filter((line) => !extras.answered.has(line)), ...extras.notes].map((line) => `${line}\n`).join(''));
438
+ }
439
+
440
+ // SWIG looks in its working directory and the interface's before its include path, and neither holds the header, so
441
+ // it reads this copy, put first on that path: the includes its package lists are inlined there, while the compiler
442
+ // still includes the real header.
443
+ function writeSwigView(headerFile, headerPath, names) {
444
+ const dir = `${state.config.paths.build}/swigview`;
445
+ const viewFile = upath.join(dir, headerPath);
446
+ fs.mkdirSync(upath.dirname(viewFile), { recursive: true });
447
+ const readInclude = (name) => fs.readFileSync(upath.join(upath.dirname(headerFile), name), 'utf8');
448
+ writeIfChanged(viewFile, inlineIncludes(fs.readFileSync(headerFile, 'utf8'), names, readInclude));
449
+ return { dir, hash: getFileHash(viewFile) };
450
+ }
451
+
224
452
  // The interface text only names the header, so its hash alone kept a bridge across header edits: the
225
- // header's own hash is part of the key.
226
- function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '') {
453
+ // header's own hash, and the inlined copy's when SWIG reads one, are part of the key.
454
+ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '', swigView = null, extras = { named: [], headerText: () => '' }) {
227
455
  if (!interfaceFilePath) {
228
456
  return null;
229
457
  }
230
458
 
231
- const bridgeHash = () => `${getFileHash(interfaceFilePath)}\n${sourceHash}`;
459
+ const bridgeHash = () => [
460
+ BRIDGE_FORMAT, getFileHash(interfaceFilePath), sourceHash, ...(swigView ? [`view:${swigView.hash}`] : []),
461
+ ].join('\n');
232
462
  const fileHash = bridgeHash();
233
463
  const cachedBridge = state.cache.bridges[interfaceFilePath];
234
- if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
464
+ if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge) && isMadeFrom(cachedBridge, fileHash)) {
235
465
  applyAsyncGuard(cachedBridge);
236
466
  return cachedBridge;
237
467
  }
238
468
 
239
469
  const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
470
+ fs.writeFileSync(keyOf(bridgeFilePath), '');
240
471
  // #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
241
472
  const swig = () => run('swig', [
242
473
  '-c++',
243
474
  '-embind',
244
475
  '-cpperraswarn',
245
476
  '-o', bridgeFilePath,
477
+ ...(swigView ? [`-I${swigView.dir}`] : []),
246
478
  ...swigIncludePath(target, sourceDir),
247
479
  interfaceFilePath,
248
- ], null, target);
480
+ ], null, toolTargetFor(target));
249
481
  try {
250
482
  swig();
251
483
  } catch (e) {
@@ -264,10 +496,31 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
264
496
  }
265
497
  }
266
498
  applyAsyncGuard(bridgeFilePath);
499
+ if (extras.named.length) addBridgeExtras(bridgeFilePath, interfaceFilePath, extras.named, extras.headerText(), swig);
500
+ reportSkippedBindings(bridgeFilePath);
267
501
 
502
+ const madeFrom = bridgeHash();
503
+ fs.writeFileSync(keyOf(bridgeFilePath), getContentHash(madeFrom));
268
504
  state.cache.bridges[interfaceFilePath] = bridgeFilePath;
269
- state.cache.hashes[interfaceFilePath] = bridgeHash();
505
+ state.cache.hashes[interfaceFilePath] = madeFrom;
270
506
  saveCache();
271
507
 
272
508
  return state.cache.bridges[interfaceFilePath];
273
509
  }
510
+
511
+ // Generate a full export catalog for the editor, without compiling or linking these extra functions.
512
+ function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
513
+ const dir = `${state.config.paths.build}/declarations`;
514
+ fs.mkdirSync(dir, { recursive: true });
515
+ const file = `${dir}/${upath.basename(interfaceFile)}`;
516
+ writeIfChanged(file, interfaceWithAllFunctions(withoutBridgeExtras(fs.readFileSync(interfaceFile, 'utf8'))));
517
+ const bridge = `${file}.cpp`;
518
+ const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
519
+ if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
520
+ fs.writeFileSync(keyOf(bridge), '');
521
+ run('swig', ['-c++', '-embind', '-cpperraswarn', '-o', bridge,
522
+ ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], null, toolTargetFor(target));
523
+ fs.writeFileSync(keyOf(bridge), hash);
524
+ }
525
+ return `${bridge}.exports.json`;
526
+ }