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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +2 -2
  3. package/package.json +7 -2
  4. package/src/actions/buildBinTools.js +0 -0
  5. package/src/actions/buildDependencies.js +15 -2
  6. package/src/actions/buildExternal.js +9 -1
  7. package/src/actions/buildJs.js +2 -9
  8. package/src/actions/buildLib.js +25 -8
  9. package/src/actions/buildNativeCommand.js +84 -0
  10. package/src/actions/buildNode.js +49 -10
  11. package/src/actions/buildTypes.js +8 -2
  12. package/src/actions/buildWasiCommand.js +10 -9
  13. package/src/actions/buildWasm.js +72 -31
  14. package/src/actions/cloudCommands.js +133 -0
  15. package/src/actions/createImportedBridges.js +71 -0
  16. package/src/actions/createInterface.js +247 -68
  17. package/src/actions/createLib.js +70 -42
  18. package/src/actions/getData.js +6 -2
  19. package/src/actions/getEnv.js +7 -0
  20. package/src/actions/getLinkInputs.js +12 -5
  21. package/src/actions/isSourceNewer.js +2 -0
  22. package/src/actions/licenses.js +143 -19
  23. package/src/actions/prepareConanDependencies.js +51 -0
  24. package/src/actions/publishBinary.js +32 -0
  25. package/src/actions/run.js +63 -66
  26. package/src/actions/runnerCommands.js +433 -0
  27. package/src/actions/writeRuntimeEntry.js +80 -0
  28. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  29. package/src/assets/cmake/command/command.cpp +1 -0
  30. package/src/assets/cmake/dist.cmake +2 -1
  31. package/src/assets/configure/install.sh +22 -0
  32. package/src/assets/cpp-runtime/node.cpp +18 -1
  33. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  34. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  35. package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
  36. package/src/assets/js-runtime/core.js +12 -0
  37. package/src/assets/node-import-hooks.mjs +61 -0
  38. package/src/assets/toolchain-digests.json +19 -19
  39. package/src/assets/wasi-runtime/http/README.md +17 -0
  40. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  41. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  42. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  43. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  44. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  46. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  47. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  48. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  49. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  50. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  51. package/src/assets/wasi-runtime/stubs.c +29 -0
  52. package/src/bin.js +218 -161
  53. package/src/cli.js +14 -0
  54. package/src/index.js +3 -0
  55. package/src/integration/bindHeaderImports.js +18 -0
  56. package/src/integration/getCrossbindScript.js +50 -13
  57. package/src/integration/getDependFilePath.js +9 -0
  58. package/src/integration/resolveNativeImport.js +14 -0
  59. package/src/node/dev.mjs +19 -0
  60. package/src/program.js +169 -0
  61. package/src/runner/blobs.js +135 -0
  62. package/src/runner/files.js +193 -0
  63. package/src/runner/server.js +320 -0
  64. package/src/state/attachConanDependencies.js +56 -0
  65. package/src/state/index.js +20 -11
  66. package/src/state/loadConfig.js +37 -35
  67. package/src/state/refreshBuiltDependencies.js +20 -0
  68. package/src/state/refreshConanDependencies.js +17 -0
  69. package/src/utils/androidToolchain.js +4 -0
  70. package/src/utils/appRustCrates.js +19 -1
  71. package/src/utils/appSources.js +97 -6
  72. package/src/utils/archiveFlags.js +12 -0
  73. package/src/utils/boundHeaders.js +79 -0
  74. package/src/utils/bridgeExtras.js +554 -0
  75. package/src/utils/bridgeRuntimeFixes.js +48 -0
  76. package/src/utils/bundledLicenses.js +26 -0
  77. package/src/utils/cargoLock.js +19 -0
  78. package/src/utils/cargoTarget.js +21 -3
  79. package/src/utils/cloudAccount.js +55 -0
  80. package/src/utils/conanDependencies.js +66 -0
  81. package/src/utils/conanImport.js +58 -0
  82. package/src/utils/conanInstall.js +154 -0
  83. package/src/utils/conanProfile.js +203 -0
  84. package/src/utils/conanStage.js +292 -0
  85. package/src/utils/copyToDistSource.js +13 -0
  86. package/src/utils/cppDts.js +60 -15
  87. package/src/utils/darwinToolchain.js +11 -0
  88. package/src/utils/dirLock.js +137 -28
  89. package/src/utils/downloadAndExtractFile.js +34 -7
  90. package/src/utils/embindArgumentGuards.js +41 -2
  91. package/src/utils/embindCallArguments.js +19 -0
  92. package/src/utils/embindJspiQueue.js +25 -0
  93. package/src/utils/embindObjectLifetime.js +35 -0
  94. package/src/utils/execContainer.js +49 -0
  95. package/src/utils/headerImports.js +16 -5
  96. package/src/utils/hostPlatform.js +21 -0
  97. package/src/utils/iosToolchain.js +6 -0
  98. package/src/utils/isSourceCmakePackage.js +10 -0
  99. package/src/utils/licenseReport.js +11 -2
  100. package/src/utils/linuxToolchain.js +24 -0
  101. package/src/utils/makeTreeWritable.js +12 -0
  102. package/src/utils/nativeImports.js +23 -0
  103. package/src/utils/nativeSourceStamp.js +36 -0
  104. package/src/utils/nodeDevBuild.js +92 -0
  105. package/src/utils/packageLicense.js +103 -0
  106. package/src/utils/pthreadMainScript.js +14 -0
  107. package/src/utils/pullDockerImage.js +3 -1
  108. package/src/utils/relocatePrebuilt.js +119 -0
  109. package/src/utils/remoteClient.js +127 -0
  110. package/src/utils/remoteOutputs.js +39 -0
  111. package/src/utils/remoteRunner.js +97 -0
  112. package/src/utils/remoteUpload.js +134 -0
  113. package/src/utils/replaceFile.js +8 -0
  114. package/src/utils/resetCMakeCacheForSource.js +26 -0
  115. package/src/utils/runCargo.js +48 -51
  116. package/src/utils/runConan.js +257 -0
  117. package/src/utils/runtimeEntries.js +56 -0
  118. package/src/utils/rustBridgeGen.js +22 -8
  119. package/src/utils/rustMt.js +9 -0
  120. package/src/utils/rustSysroot.js +7 -5
  121. package/src/utils/scopedEmbind.js +22 -0
  122. package/src/utils/selectRunner.js +81 -0
  123. package/src/utils/sourceFingerprint.js +4 -3
  124. package/src/utils/swigInterface.js +70 -4
  125. package/src/utils/systemConfig.js +13 -0
  126. package/src/utils/systemKeys.js +35 -3
  127. package/src/utils/targets.js +60 -4
  128. package/src/utils/toolchainNotices.js +5 -0
  129. package/src/utils/wasiHttp.js +50 -0
  130. package/src/utils/wasiToolchain.js +14 -1
  131. package/src/utils/windowsToolchain.js +18 -0
  132. package/vitest.config.js +11 -0
@@ -1,29 +1,60 @@
1
1
 
2
2
  import fs from 'node:fs';
3
- import { createRequire } from 'node:module';
4
3
  import upath from 'upath';
5
4
  import state, { saveCache } from '../state/index.js';
5
+ import refreshConanDependencies from '../state/refreshConanDependencies.js';
6
+ import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
7
+ import { conanInputsOf } from '../utils/conanDependencies.js';
6
8
  import { getContentHash, getFileHash } from '../utils/hash.js';
7
9
  import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
8
- import getDependFilePath from '../integration/getDependFilePath.js';
9
- import { writeHeaderDts } from '../utils/cppDts.js';
10
+ import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
11
+ import {
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';
10
16
  import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
11
17
  import {
12
- buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, indexTypeDefinitions, interfaceIncludes,
13
- interfaceToRetryWithoutMacros, parseMacroDump, referencedTypeHeaders, selectSwigMacros,
18
+ buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
19
+ indexTypeDefinitions, inlineIncludes, interfaceIncludes, interfaceToRetryWithoutMacros, interfaceWithAllFunctions, parseMacroDump, referencedTypeHeaders,
20
+ selectSwigMacros,
14
21
  } from '../utils/swigInterface.js';
15
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';
16
26
  import run, { cxxPreprocessorFor } from './run.js';
27
+ import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
17
28
 
18
29
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
19
- const INTERFACE_FORMAT = 'swig-macros-1';
30
+ const INTERFACE_FORMAT = 'swig-macros-4';
20
31
  // Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
21
32
  // bridges from a fork before field bindings and constants are rebuilt.
22
- const BRIDGE_FORMAT = 'swig-constants-1';
33
+ const BRIDGE_FORMAT = 'swig-extras-2';
23
34
  const predefinedMacros = new Map();
24
35
  const typeDefinitions = new Map();
25
36
 
26
- 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);
27
58
  const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
28
59
  if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
29
60
  fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
@@ -34,26 +65,42 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
34
65
  // The header may live outside paths.native (e.g. a package-subpath import like
35
66
  // '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
36
67
  const sourceDir = upath.dirname(interfaceFilePath);
37
- const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir);
38
- 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);
39
80
  const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
40
81
  if (bridgeFile) {
41
82
  // Records which header this bridge came from, so directory-driven consumers
42
83
  // (getAllBridges) can prune bridges whose header no longer exists.
43
84
  writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
44
- writeHeaderDts({
85
+ const dtsOptions = {
45
86
  headerFile: interfaceFilePath,
46
87
  exportsFile: `${bridgeFile}.exports.json`,
47
88
  projectPath: state.config.paths.project,
48
89
  cacheDir: state.config.paths.cache,
49
90
  dtsMode: state.config.dts,
50
- });
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);
51
97
  }
52
98
  if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
53
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.
54
101
  const dependencyBridges = dependencyHeadersOf(interfaceFilePath, target).map((header) => {
55
102
  try {
56
- return createBridgeFile(header, target, { withDependencies: false });
103
+ return createBridgeFile(header, target, { withDependencies: false, wholeHeaders });
57
104
  } catch (e) {
58
105
  console.warn(`crossbind: ${upath.basename(interfaceFilePath)} uses types from ${upath.basename(header)}, whose bindings failed (${e.message})`);
59
106
  return null;
@@ -64,7 +111,36 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
64
111
  return bridgeFile;
65
112
  }
66
113
 
67
- 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 = [] }) {
68
144
  if (!headerOrModuleFilePath) {
69
145
  return null;
70
146
  }
@@ -74,64 +150,70 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
74
150
  const packages = [state.config, ...state.config.allDependencies];
75
151
  const prelude = isModule ? [] : findHeaderPrelude(headerOrModuleFilePath, packages, headerPath);
76
152
  const ignored = isModule ? [] : findIgnoredDeclarations(headerOrModuleFilePath, packages, headerPath);
153
+ const preamble = isModule ? [] : findSwigPreamble(headerOrModuleFilePath, packages, headerPath);
77
154
  const completing = includeRoot ? findCompletingIncludes(headerOrModuleFilePath, includeRoot, headerPath) : [];
78
155
  const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
79
156
  const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
80
- const constants = isModule ? [] : importedNames(headerOrModuleFilePath, target);
81
157
  // 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.
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.
84
160
  const fileHash = [
85
161
  INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
86
162
  ...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
163
+ ...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
87
164
  ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
88
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(),
89
169
  ].join('\n');
90
170
  const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
91
171
  if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
92
- && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
172
+ && isMadeFrom(cachedInterface, fileHash) && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
93
173
  return cachedInterface;
94
174
  }
95
175
 
96
176
  if (isModule) {
97
177
  const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
98
- fs.copyFileSync(headerOrModuleFilePath, newPath);
99
- state.cache.interfaces[headerOrModuleFilePath] = newPath;
100
- state.cache.hashes[headerOrModuleFilePath] = fileHash;
101
- saveCache();
102
- return newPath;
178
+ return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
103
179
  }
104
180
 
105
181
  if (!filePathWithoutExt) return null;
106
182
 
107
183
  if (fs.existsSync(interfaceFile)) {
108
184
  const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
109
- fs.copyFileSync(interfaceFile, newPath);
110
- state.cache.interfaces[headerOrModuleFilePath] = newPath;
111
- state.cache.hashes[headerOrModuleFilePath] = fileHash;
112
- saveCache();
113
- return newPath;
185
+ return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
114
186
  }
115
187
 
116
188
  const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
117
189
 
190
+ const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
191
+ const extras = interfaceExtras(named, macros.table, fs.readFileSync(headerOrModuleFilePath, 'utf8'));
118
192
  const content = buildInterfaceContent({
119
193
  moduleName: fileName.toUpperCase(),
120
194
  headerPath,
121
195
  prelude,
122
196
  completing,
123
- swigMacros: collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants),
197
+ swigMacros: macros.lines,
124
198
  ignored,
125
199
  constants,
200
+ preamble,
201
+ functions: functions === ALL_NAMES ? ALL_NAMES : [...functions, ...extras.functions],
202
+ extras: extras.lines,
126
203
  });
127
204
  const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
128
- fs.writeFileSync(outputFilePath, content);
205
+ return recordInterface(headerOrModuleFilePath, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
206
+ }
129
207
 
130
- state.cache.interfaces[headerOrModuleFilePath] = outputFilePath;
131
- 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;
132
215
  saveCache();
133
-
134
- return outputFilePath;
216
+ return interfaceFile;
135
217
  }
136
218
 
137
219
  const interfacePath = (name) => `${state.config.paths.build}/interface/${name}.i`;
@@ -157,10 +239,11 @@ function includeLocation(headerFile, target) {
157
239
  const dependencyRoots = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
158
240
  .filter((d) => d.startsWith(state.config.paths.base));
159
241
  const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${escapeRegExp(p)}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
160
- return match ? { includeRoot: match[1], headerPath: match[2] } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
242
+ return match ? { includeRoot: match[1], headerPath: match[2], isDependency: true } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
161
243
  }
162
244
 
163
- // 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/).
164
247
  function definitionsUnder(includeRoot) {
165
248
  if (!typeDefinitions.has(includeRoot)) {
166
249
  const extensions = new RegExp(`\\.(${state.config.ext.header.join('|')})$`, 'i');
@@ -169,22 +252,35 @@ function definitionsUnder(includeRoot) {
169
252
  .map((entry) => upath.relative(includeRoot, upath.join(entry.parentPath, entry.name)))
170
253
  .sort()
171
254
  .map((path) => ({ path, text: fs.readFileSync(upath.join(includeRoot, path), 'utf8') }));
172
- 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
+ });
173
264
  }
174
265
  return typeDefinitions.get(includeRoot);
175
266
  }
176
267
 
268
+ function lookupContext(includeRoot, headerPath, headerText) {
269
+ const { definitions, copyOf } = definitionsUnder(includeRoot);
270
+ return { headerText, headerPath: copyOf.get(headerPath) ?? headerPath, definitions };
271
+ }
272
+
177
273
  function findCompletingIncludes(headerFile, includeRoot, headerPath) {
178
274
  const headerText = fs.readFileSync(headerFile, 'utf8');
179
275
  if (!headerText.includes('unique_ptr')) return [];
180
- return completingIncludes({ headerText, headerPath, definitions: definitionsUnder(includeRoot) });
276
+ return completingIncludes(lookupContext(includeRoot, headerPath, headerText));
181
277
  }
182
278
 
183
279
  function dependencyHeadersOf(headerFile, target) {
184
280
  const { includeRoot, headerPath } = includeLocation(headerFile, target);
185
281
  if (!includeRoot) return [];
186
282
  const headerText = fs.readFileSync(headerFile, 'utf8');
187
- return referencedTypeHeaders({ headerText, headerPath, definitions: definitionsUnder(includeRoot) })
283
+ return referencedTypeHeaders(lookupContext(includeRoot, headerPath, headerText))
188
284
  .map((header) => upath.join(includeRoot, header));
189
285
  }
190
286
 
@@ -204,29 +300,33 @@ function realPath(file) {
204
300
  }
205
301
  }
206
302
 
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) {
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) {
220
309
  const projectDir = state.config.paths.project;
221
- if (!projectDir || !fs.existsSync(projectDir)) return [];
310
+ if (!projectDir || !fs.existsSync(projectDir)) return { names: [], takesModule: false, named: [] };
222
311
  const header = targetNeutral(realPath(headerFile));
223
312
  const names = new Set();
313
+ let isImported = false;
314
+ let takesAllSymbols = false;
315
+ let takesWhole = false;
224
316
  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));
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));
228
326
  }
229
- return [...names].sort();
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 };
230
330
  }
231
331
 
232
332
  // A header that does not preprocess on its own, or a host without the image's compiler, keeps the plain interface.
@@ -237,10 +337,13 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
237
337
  predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
238
338
  }
239
339
  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 });
340
+ return {
341
+ lines: selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants }),
342
+ table: macros,
343
+ };
241
344
  } catch (e) {
242
345
  console.warn(`crossbind: SWIG reads ${upath.basename(headerFile)} without the macros of its includes (${e.message})`);
243
- return [];
346
+ return { lines: [], table: new Map() };
244
347
  }
245
348
  }
246
349
 
@@ -249,14 +352,20 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
249
352
  // it, so NDEBUG counts as a macro SWIG needs rather than a compiler predefine.
250
353
  const RELEASE_DEFINE = '-DNDEBUG';
251
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
+
252
360
  function dumpMacros(outputFile, includes, includePath, target, defines = []) {
253
- run(cxxPreprocessorFor(target), [
361
+ const tool = toolTargetFor(target);
362
+ run(cxxPreprocessorFor(tool), [
254
363
  '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
255
364
  ...includePath,
256
365
  ...includes.flatMap((header) => ['-include', header]),
257
366
  '-o', outputFile,
258
367
  '/dev/null',
259
- ], null, target);
368
+ ], null, tool);
260
369
  return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
261
370
  }
262
371
 
@@ -267,6 +376,9 @@ function swigIncludePath(target, sourceDir) {
267
376
  ...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
268
377
  ...state.config.paths.header,
269
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),
270
382
  ...(sourceDir ? [sourceDir] : []),
271
383
  ].filter((path) => !!path.toString()).map((path) => `-I${path}`);
272
384
  return [...new Set(includePath)];
@@ -278,7 +390,7 @@ function swigIncludePath(target, sourceDir) {
278
390
  // next time they are picked up.
279
391
  function applyAsyncGuard(bridgeFilePath) {
280
392
  const bridgeText = fs.readFileSync(bridgeFilePath, { encoding: 'utf8' });
281
- const guarded = guardAsyncBindings(bridgeText);
393
+ const guarded = guardAsyncBindings(fixBridgeRuntime(bridgeText));
282
394
  if (guarded !== bridgeText) {
283
395
  fs.writeFileSync(bridgeFilePath, guarded);
284
396
  }
@@ -294,31 +406,78 @@ function reportSkippedBindings(bridgeFilePath) {
294
406
  });
295
407
  }
296
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
+
297
452
  // The interface text only names the header, so its hash alone kept a bridge across header edits: the
298
- // header's own hash is part of the key.
299
- 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: () => '' }) {
300
455
  if (!interfaceFilePath) {
301
456
  return null;
302
457
  }
303
458
 
304
- const bridgeHash = () => `${BRIDGE_FORMAT}\n${getFileHash(interfaceFilePath)}\n${sourceHash}`;
459
+ const bridgeHash = () => [
460
+ BRIDGE_FORMAT, getFileHash(interfaceFilePath), sourceHash, ...(swigView ? [`view:${swigView.hash}`] : []),
461
+ ].join('\n');
305
462
  const fileHash = bridgeHash();
306
463
  const cachedBridge = state.cache.bridges[interfaceFilePath];
307
- if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
464
+ if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge) && isMadeFrom(cachedBridge, fileHash)) {
308
465
  applyAsyncGuard(cachedBridge);
309
466
  return cachedBridge;
310
467
  }
311
468
 
312
469
  const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
470
+ fs.writeFileSync(keyOf(bridgeFilePath), '');
313
471
  // #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
314
472
  const swig = () => run('swig', [
315
473
  '-c++',
316
474
  '-embind',
317
475
  '-cpperraswarn',
318
476
  '-o', bridgeFilePath,
477
+ ...(swigView ? [`-I${swigView.dir}`] : []),
319
478
  ...swigIncludePath(target, sourceDir),
320
479
  interfaceFilePath,
321
- ], null, target);
480
+ ], null, toolTargetFor(target));
322
481
  try {
323
482
  swig();
324
483
  } catch (e) {
@@ -337,11 +496,31 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
337
496
  }
338
497
  }
339
498
  applyAsyncGuard(bridgeFilePath);
499
+ if (extras.named.length) addBridgeExtras(bridgeFilePath, interfaceFilePath, extras.named, extras.headerText(), swig);
340
500
  reportSkippedBindings(bridgeFilePath);
341
501
 
502
+ const madeFrom = bridgeHash();
503
+ fs.writeFileSync(keyOf(bridgeFilePath), getContentHash(madeFrom));
342
504
  state.cache.bridges[interfaceFilePath] = bridgeFilePath;
343
- state.cache.hashes[interfaceFilePath] = bridgeHash();
505
+ state.cache.hashes[interfaceFilePath] = madeFrom;
344
506
  saveCache();
345
507
 
346
508
  return state.cache.bridges[interfaceFilePath];
347
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
+ }