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
@@ -0,0 +1,292 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import upath from 'upath';
4
+ import loadJson from './loadJson.js';
5
+ import writeIfChanged from './writeIfChanged.js';
6
+ import makeTreeWritable from './makeTreeWritable.js';
7
+ import logger from './logger.js';
8
+ import { libNameOf } from './linkLayout.js';
9
+ import { CONAN_NAME, conanPackageDir } from './conanImport.js';
10
+ import { CONAN_VERSION } from './conanDependencies.js';
11
+
12
+ const MANIFESTS = 'manifests';
13
+ const LIB_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
14
+ // A macro name with a plain value, if any (a path or a list too): one compiler argument, in a list CMake
15
+ // splits on ';'.
16
+ const DEFINE = /^[A-Za-z_]\w*(=[\w.+/,:-]*)?$/;
17
+ // What may follow name/version in a reference: a user/channel and a recipe revision.
18
+ const REFERENCE_TAIL = /^(@[A-Za-z0-9_][A-Za-z0-9_.+-]*\/[A-Za-z0-9_][A-Za-z0-9_.+-]*)?(#[0-9a-f]+)?$/;
19
+ const URL = /^https?:\/\/[^\s"'<>`\\]+$/;
20
+ const SHA256 = /^[0-9a-f]{64}$/;
21
+ const MAX_TEXT = 1000;
22
+ const TARGET_PATH = /^[A-Za-z0-9_-]+$/;
23
+ // The prefix of the variables a package's CMakeLists names each target's libraries by.
24
+ const HOST_LIBS = 'CROSSBIND_CONAN_LIBS_';
25
+
26
+ // The CMake target of a package; Conan names may carry '.', '+' and '-'.
27
+ export const conanTargetName = (name) => `conan_${name.replace(/[^A-Za-z0-9_]/g, '_')}`;
28
+
29
+ const matches = (pattern, value) => typeof value === 'string' && pattern.test(value);
30
+ const isListOf = (pattern, values) => Array.isArray(values) && values.every((value) => matches(pattern, value));
31
+ const shown = (value) => `${JSON.stringify(value)}`.slice(0, 200);
32
+ // C0 and C1 controls, which carry terminal escape sequences.
33
+ export const isControlCharacter = (char) => char < ' ' || (char >= '\u007f' && char <= '\u009f');
34
+ // Recipe text ends up in notices and terminals: one line, no control characters.
35
+ const textOf = (value) => (typeof value === 'string' && value.length <= MAX_TEXT && ![...value].some(isControlCharacter) ? value : null);
36
+ const urlOf = (value) => (matches(URL, value) && value.length <= MAX_TEXT ? value : null);
37
+
38
+ // A list means every license applies; an item that offers a choice stays one term of it.
39
+ function licenseOf(license) {
40
+ const parts = Array.isArray(license) ? license.map(textOf) : [textOf(license)];
41
+ if (parts.length === 0 || !parts.every(Boolean)) return null;
42
+ return parts.length === 1 ? parts[0] : parts.map((part) => (part.includes(' OR ') ? `(${part})` : part)).join(' AND ');
43
+ }
44
+
45
+ // The fields of a package that crossbind keeps. Conan's JSON comes from recipe code and these fields
46
+ // end up in paths, CMake files and notices, so they are checked where they come in and again where
47
+ // the manifest is read back.
48
+ export function conanPackageEntry(entry) {
49
+ const fail = (field) => {
50
+ throw new Error(`crossbind: the Conan package ${shown(entry.ref ?? entry.name)} has an invalid ${field}: ${shown(entry[field])}.`);
51
+ };
52
+ if (!matches(CONAN_NAME, entry.name)) fail('name');
53
+ if (!matches(CONAN_VERSION, entry.version)) fail('version');
54
+ const reference = `${entry.name}/${entry.version}`;
55
+ if (typeof entry.ref !== 'string' || !entry.ref.startsWith(reference) || !REFERENCE_TAIL.test(entry.ref.slice(reference.length))) fail('ref');
56
+ if (!isListOf(LIB_NAME, entry.libs)) fail('libs');
57
+ if (!isListOf(CONAN_NAME, entry.requires)) fail('requires');
58
+ // They become linker and compiler arguments. A manifest an older crossbind wrote has none of them.
59
+ const systemLibs = entry.systemLibs ?? [];
60
+ const frameworks = entry.frameworks ?? [];
61
+ const defines = entry.defines ?? [];
62
+ if (!isListOf(LIB_NAME, systemLibs)) fail('systemLibs');
63
+ if (!isListOf(LIB_NAME, frameworks)) fail('frameworks');
64
+ if (!isListOf(DEFINE, defines)) fail('defines');
65
+ const url = urlOf(entry.source?.url);
66
+ return {
67
+ name: entry.name,
68
+ version: entry.version,
69
+ ref: entry.ref,
70
+ license: licenseOf(entry.license),
71
+ homepage: urlOf(entry.homepage),
72
+ source: url ? { url, sha256: matches(SHA256, entry.source.sha256) ? entry.source.sha256 : null } : null,
73
+ libs: [...entry.libs],
74
+ requires: [...entry.requires],
75
+ systemLibs: [...systemLibs],
76
+ frameworks: [...frameworks],
77
+ defines: [...defines],
78
+ };
79
+ }
80
+
81
+ function sourceOf(entry) {
82
+ const source = Array.isArray(entry) ? entry[0] : entry;
83
+ if (!source?.url) return null;
84
+ return { url: Array.isArray(source.url) ? source.url[0] : source.url, sha256: source.sha256 ?? null };
85
+ }
86
+
87
+ // A define crossbind cannot pass as one plain argument was left out before crossbind took defines at all,
88
+ // so it stays out, with a notice, instead of stopping the build.
89
+ function plainDefinesOf(node, defines) {
90
+ return defines.filter((define) => {
91
+ if (matches(DEFINE, define)) return true;
92
+ logger.info(`crossbind: ${shown(node.ref)} asks the code that uses it to compile with ${shown(define)}, which crossbind leaves out: a define passes as a plain name with a plain value.`);
93
+ return false;
94
+ });
95
+ }
96
+
97
+ function packageOf(node, requires) {
98
+ const components = Object.values(node.cpp_info ?? {});
99
+ const collect = (key) => [...new Set(components.flatMap((component) => component?.[key] ?? []))];
100
+ const folders = (key) => {
101
+ const values = collect(key);
102
+ if (!values.every((value) => typeof value === 'string')) {
103
+ throw new Error(`crossbind: the Conan package ${shown(node.ref)} has invalid ${key}.`);
104
+ }
105
+ return values;
106
+ };
107
+ if (typeof node.package_folder !== 'string') {
108
+ throw new Error(`crossbind: conan reported the package ${shown(node.ref)} without a package folder.`);
109
+ }
110
+ return {
111
+ ...conanPackageEntry({
112
+ name: node.name,
113
+ version: node.version,
114
+ ref: node.ref,
115
+ license: node.license,
116
+ homepage: node.homepage,
117
+ source: sourceOf(node.conandata?.sources?.[node.version]),
118
+ libs: collect('libs'),
119
+ requires,
120
+ systemLibs: collect('system_libs'),
121
+ frameworks: collect('frameworks'),
122
+ defines: plainDefinesOf(node, collect('defines')),
123
+ }),
124
+ packageFolder: node.package_folder,
125
+ includedirs: folders('includedirs'),
126
+ libdirs: folders('libdirs'),
127
+ };
128
+ }
129
+
130
+ // One entry per package name, each before the packages it requires: the order a static link takes
131
+ // them in.
132
+ function linkOrder(packages) {
133
+ const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
134
+ const order = [];
135
+ const visited = new Set();
136
+ const visit = (name) => {
137
+ if (visited.has(name) || !byName.has(name)) return;
138
+ visited.add(name);
139
+ byName.get(name).requires.forEach(visit);
140
+ order.push(byName.get(name));
141
+ };
142
+ byName.forEach((pkg, name) => visit(name));
143
+ return order.reverse();
144
+ }
145
+
146
+ // Host packages of a `conan install --format=json` graph in link order. Left out are the consumer,
147
+ // build tools, test requirements and packages conan skipped because the consumer needs nothing of them.
148
+ export function conanPackagesOf(graph) {
149
+ const nodes = graph?.nodes;
150
+ if (!nodes || typeof nodes !== 'object' || !graph.root) {
151
+ throw new Error('crossbind: conan install printed a graph without nodes or a root.');
152
+ }
153
+ const [root] = Object.keys(graph.root);
154
+ const isPackage = (id) => id !== root && nodes[id]?.context === 'host' && !nodes[id].test && nodes[id].binary !== 'Skip';
155
+ const ids = Object.keys(nodes).filter(isPackage);
156
+ const requiresOf = (id) => Object.entries(nodes[id].dependencies ?? {})
157
+ .filter(([dependency, edge]) => !edge?.build && !edge?.test && ids.includes(dependency))
158
+ .map(([dependency]) => nodes[dependency].name);
159
+ return linkOrder(ids.map((id) => packageOf(nodes[id], requiresOf(id))));
160
+ }
161
+
162
+ // The real path of a file conan reported, or null when there is none; refused when it resolves
163
+ // outside `root`.
164
+ function realPathWithin(file, root, pkg) {
165
+ if (!fs.existsSync(file)) return null;
166
+ const real = fs.realpathSync(file);
167
+ const relative = path.relative(root, real);
168
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
169
+ throw new Error(`crossbind: the Conan package ${pkg.ref} reports ${file}, which resolves outside ${root}.`);
170
+ }
171
+ return real;
172
+ }
173
+
174
+ // Packages link files to each other (libpng.a -> libpng16.a), so links are followed, but only to
175
+ // directories and regular files inside the package, and into each linked directory once: a link out
176
+ // of the package could point the copy at any file the host can read, and links back up the tree
177
+ // would copy it without end.
178
+ function copyTree(from, to, packageRoot, pkg) {
179
+ const linked = new Set([from]);
180
+ fs.cpSync(from, to, {
181
+ recursive: true,
182
+ dereference: true,
183
+ filter: (source) => {
184
+ const entry = fs.lstatSync(source);
185
+ if (!entry.isSymbolicLink()) return entry.isDirectory() || entry.isFile();
186
+ const real = realPathWithin(source, packageRoot, pkg);
187
+ const target = real && fs.statSync(real);
188
+ if (!target?.isDirectory()) return Boolean(target?.isFile());
189
+ if (linked.has(real)) return false;
190
+ linked.add(real);
191
+ return true;
192
+ },
193
+ });
194
+ makeTreeWritable(to);
195
+ }
196
+
197
+ // The package's own folder: no link, and below the store's own layout, so it cannot take in the
198
+ // store or another package.
199
+ function packageRootOf(pkg, folder, store) {
200
+ const real = realPathWithin(folder, store, pkg);
201
+ if (!real || fs.lstatSync(folder).isSymbolicLink() || path.relative(store, real).split(path.sep).length < 2) {
202
+ throw new Error(`crossbind: the Conan package ${pkg.ref} has no package folder crossbind can use at ${pkg.packageFolder}.`);
203
+ }
204
+ return real;
205
+ }
206
+
207
+ // A package lands where a published port keeps its prebuilt (<dir>/dist/prebuilt/<target>/{include,lib}),
208
+ // so the dependency machinery links it and SWIG treats its headers as one across targets. Everything
209
+ // is read from inside the package's own folder in the store.
210
+ export function stageConanPackage(pkg, {
211
+ stageDir, targetPath, toHost, store, distCmake,
212
+ }) {
213
+ const packageRoot = packageRootOf(pkg, toHost(pkg.packageFolder), fs.realpathSync(store));
214
+ const reported = (folder) => realPathWithin(toHost(upath.isAbsolute(folder) ? folder : upath.join(pkg.packageFolder, folder)), packageRoot, pkg);
215
+ const dir = conanPackageDir(stageDir, pkg.name);
216
+ const prebuilt = upath.join(dir, 'dist', 'prebuilt');
217
+ const prefix = upath.join(prebuilt, targetPath);
218
+
219
+ fs.rmSync(prefix, { recursive: true, force: true });
220
+ fs.mkdirSync(upath.join(prefix, 'include'), { recursive: true });
221
+ fs.mkdirSync(upath.join(prefix, 'lib'), { recursive: true });
222
+ pkg.includedirs.map(reported).filter(Boolean).forEach((folder) => copyTree(folder, upath.join(prefix, 'include'), packageRoot, pkg));
223
+ const libdirs = pkg.libdirs.map(reported).filter(Boolean);
224
+ pkg.libs.forEach((lib) => {
225
+ const archive = libdirs.map((folder) => realPathWithin(path.join(folder, `lib${lib}.a`), packageRoot, pkg))
226
+ .find((file) => file && fs.statSync(file).isFile());
227
+ if (!archive) {
228
+ throw new Error(`crossbind: the Conan package ${pkg.ref} declares the library ${lib}, but its package has no lib${lib}.a.`);
229
+ }
230
+ fs.copyFileSync(archive, upath.join(prefix, 'lib', `lib${lib}.a`));
231
+ });
232
+
233
+ const licenses = upath.join(dir, 'licenses');
234
+ fs.rmSync(licenses, { recursive: true, force: true });
235
+ const shipped = realPathWithin(path.join(packageRoot, 'licenses'), packageRoot, pkg);
236
+ if (shipped && fs.statSync(shipped).isDirectory()) copyTree(shipped, licenses, packageRoot, pkg);
237
+ writeIfChanged(upath.join(dir, 'package.json'), `${JSON.stringify({
238
+ name: `conan:${pkg.name}`, version: pkg.version, nativeVersion: pkg.version, license: pkg.license, private: true,
239
+ }, null, 4)}\n`);
240
+
241
+ const hosts = fs.readdirSync(prebuilt, { withFileTypes: true })
242
+ .filter((entry) => entry.isDirectory() && TARGET_PATH.test(entry.name)).map((entry) => entry.name).sort();
243
+ // A recipe can name its libraries otherwise on another platform (libpng is png16 on Windows), so each
244
+ // target links the archives staged for it.
245
+ const archivesOf = (host) => {
246
+ const libdir = upath.join(prebuilt, host, 'lib');
247
+ if (!fs.existsSync(libdir)) return [];
248
+ return fs.readdirSync(libdir).filter((file) => file.endsWith('.a')).map(libNameOf).filter((name) => matches(LIB_NAME, name)).sort();
249
+ };
250
+ writeIfChanged(upath.join(prebuilt, 'CMakeLists.txt'), [
251
+ ...hosts.map((host) => `set(${HOST_LIBS}${host} "${archivesOf(host).join(';')}")`),
252
+ distCmake
253
+ .replace('___PROJECT_NAME___', () => conanTargetName(pkg.name))
254
+ .replace('___PROJECT_HOST___', () => hosts.join(';'))
255
+ .replace('___PROJECT_LIBS___', () => `\${${HOST_LIBS}\${PROJECT_TARGET_HOST}}`)
256
+ .replace('___PROJECT_WHOLE_ARCHIVE___', () => ''),
257
+ ].join('\n'));
258
+ }
259
+
260
+ const manifestsOf = (stageDir) => upath.join(stageDir, MANIFESTS);
261
+ const manifestFileOf = (stageDir, targetPath) => upath.join(manifestsOf(stageDir), `${targetPath}.json`);
262
+
263
+ // One manifest per target: a recipe can require other packages on another platform.
264
+ export function writeConanManifest(stageDir, targetPath, manifest) {
265
+ writeIfChanged(manifestFileOf(stageDir, targetPath), `${JSON.stringify(manifest, null, 4)}\n`);
266
+ }
267
+
268
+ // The packages staged for one target, or for every target staged so far, in link order, and each
269
+ // target's own list. A manifest staged for other conanDependencies describes packages the config no
270
+ // longer asks for.
271
+ export function readConanManifest(stageDir, key, targetPath = null) {
272
+ const dir = manifestsOf(stageDir);
273
+ let files = [];
274
+ if (targetPath) files = [manifestFileOf(stageDir, targetPath)];
275
+ else if (fs.existsSync(dir)) files = fs.readdirSync(dir).filter((name) => name.endsWith('.json')).sort().map((name) => upath.join(dir, name));
276
+ const manifests = files.map((file) => ({ target: upath.basename(file, '.json'), manifest: loadJson(file) }))
277
+ .filter(({ manifest }) => manifest?.key === key);
278
+ if (manifests.length === 0) return null;
279
+ try {
280
+ const targets = Object.fromEntries(manifests.map(({ target, manifest }) => [target, manifest.packages.map(conanPackageEntry)]));
281
+ return { key, packages: linkOrder(Object.values(targets).flat()), targets };
282
+ } catch (e) {
283
+ throw new Error(`crossbind: ${dir} lists a package crossbind will not use - delete ${stageDir} and build again. ${e.message}`, { cause: e });
284
+ }
285
+ }
286
+
287
+ // Changes whenever a build stages packages, so a process that attached before can tell.
288
+ export function conanManifestsStamp(stageDir) {
289
+ const dir = manifestsOf(stageDir);
290
+ if (!fs.existsSync(dir)) return '';
291
+ return fs.readdirSync(dir).sort().map((name) => `${name}@${fs.statSync(upath.join(dir, name)).mtimeMs}`).join('|');
292
+ }
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import upath from 'upath';
4
+
5
+ const NODE_MODULES = 'node_modules/';
6
+
7
+ // A copyToDist source names another package's file as node_modules/<package>/<file>. npm may hoist that package
8
+ // above the one being built, so a path missing beside the package resolves the way Node finds the package.
9
+ export default function copyToDistSource(projectDir, key) {
10
+ const local = upath.join(projectDir, key);
11
+ if (fs.existsSync(local) || !key.startsWith(NODE_MODULES)) return local;
12
+ return upath.normalize(createRequire(upath.join(projectDir, 'package.json')).resolve(key.slice(NODE_MODULES.length)));
13
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { stripComments } from './swigInterface.js';
4
4
  import writeIfChanged from './writeIfChanged.js';
5
+ import { conanImportOfHeader, conanStageDir } from './conanImport.js';
5
6
 
6
7
  // Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
7
8
  // binding-rules surface (classes, public methods, primitives/string/shared_ptr), skip
@@ -34,7 +35,7 @@ function tsType(raw, classNames, { isReturn = false } = {}) {
34
35
  if (t === 'void') return 'void';
35
36
  if (t === 'bool') return 'boolean';
36
37
  if (NUMBER_TYPES.has(t)) return 'number';
37
- if (t === 'std::string') return 'string';
38
+ if (t === 'std::string' || t === 'std::u16string') return 'string';
38
39
  if (classNames.has(t)) return t;
39
40
  return null;
40
41
  }
@@ -133,8 +134,12 @@ function typedefNamesAfter(text, from) {
133
134
  return tail ? tail[1].split(',').map((part) => part.trim()).filter((part) => /^[A-Za-z_]\w*$/.test(part)) : [];
134
135
  }
135
136
 
137
+ // A brace inside a string or character literal is text; the model needs no literal's contents.
138
+ const LITERAL = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g;
139
+ const blankLiterals = (text) => text.replace(LITERAL, (literal) => `${literal[0]}${' '.repeat(literal.length - 2)}${literal[0]}`);
140
+
136
141
  export function parseCppSurface(source, log = console.log) {
137
- const clean = stripComments(source).replace(/^[ \t]*#[^\n]*$/gm, ' ');
142
+ const clean = blankLiterals(stripComments(source)).replace(/^[ \t]*#[^\n]*$/gm, ' ');
138
143
 
139
144
  const classes = [];
140
145
  const classNames = new Set();
@@ -154,13 +159,18 @@ export function parseCppSurface(source, log = console.log) {
154
159
  for (const cls of found) {
155
160
  let access = cls.kind === 'class' ? 'private' : 'public';
156
161
  let ctor = null;
162
+ let declaresCtor = false;
163
+ let isAbstract = false;
157
164
  const methods = [];
158
165
  const fields = [];
159
166
  for (let statement of bodyStatements(cls.body)) {
160
167
  const sections = statement.match(/\b(public|private|protected)\s*:\s*([\s\S]*)$/);
161
168
  if (sections) { access = sections[1]; statement = sections[2].trim(); }
169
+ if (new RegExp(`^(?:explicit\\s+)?${cls.name}\\s*\\(`).test(statement)) declaresCtor = true;
170
+ if (/\)\s*(?:const\s*)?(?:override\s*)?=\s*0$/.test(statement)) isAbstract = true;
162
171
  if (!statement || access !== 'public') continue;
163
- statement = statement.replace(/\)\s*:\s*[\s\S]*$/, ')').replace(/\)\s*const$/, ')').trim();
172
+ const isDeleted = /=\s*delete$/.test(statement);
173
+ statement = statement.replace(/\s*=\s*(?:default|delete)$/, '').replace(/\)\s*:\s*[\s\S]*$/, ')').replace(/\)\s*const$/, ')').trim();
164
174
 
165
175
  const sig = statement.match(/^(static\s+)?(?:explicit\s+)?([\w:<>,\s*&]*?)\s*\b([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/);
166
176
  if (!sig) {
@@ -172,12 +182,17 @@ export function parseCppSurface(source, log = console.log) {
172
182
  const [, staticKw, retRaw, name, argsRaw] = sig;
173
183
  const args = parseArgs(argsRaw, classNames);
174
184
  if (args.includes(null)) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported parameter type)`); continue; }
175
- if (name === cls.name && retRaw.trim() === '') { ctor = { args }; continue; }
185
+ if (name === cls.name && retRaw.trim() === '') {
186
+ if (!isDeleted) ctor = { args };
187
+ continue;
188
+ }
176
189
  if (name.startsWith('~')) continue;
177
190
  const ret = tsType(retRaw, classNames, { isReturn: true });
178
191
  if (ret === null) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported return type '${retRaw.trim()}')`); continue; }
179
192
  methods.push({ name, isStatic: Boolean(staticKw), args, ret });
180
193
  }
194
+ // The bridge binds the implicit default constructor of a class that declares none, unless it is abstract.
195
+ if (!declaresCtor && !isAbstract) ctor = { args: [] };
181
196
  classes.push({ name: cls.name, aliases: cls.aliases, ctor, methods, fields });
182
197
  }
183
198
  return { classes };
@@ -210,6 +225,8 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
210
225
  for (const method of cls.methods) {
211
226
  out.push(` ${method.isStatic ? 'static ' : ''}${method.name}(${method.args.map((a) => `${a.name}: ${a.type}`).join(', ')}): ${wrap(method.ret)};`);
212
227
  }
228
+ // Frees the object now instead of when the garbage collector reclaims the handle (lifecycle-and-types.md).
229
+ out.push(` delete(): ${wrap('void')};`);
213
230
  out.push('}');
214
231
  }
215
232
  for (const name of exportNames) {
@@ -221,20 +238,48 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
221
238
  return out.join('\n');
222
239
  }
223
240
 
224
- // Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
225
- // user's header. Package headers are skipped: their types ship with the package itself.
226
- export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
227
- if (!fs.existsSync(exportsFile)) return;
228
- const relative = path.relative(projectPath, headerFile);
229
- if (relative.startsWith('..')) return;
230
- let exportNames;
241
+ function readExportNames(exportsFile, log) {
242
+ if (!fs.existsSync(exportsFile)) return null;
231
243
  try {
232
- exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
244
+ const exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
245
+ return Array.isArray(exportNames) ? exportNames : null;
233
246
  } catch (e) {
234
247
  log(`crossbind: dts: unreadable exports file ${exportsFile} (${e.message})`);
235
- return;
248
+ return null;
236
249
  }
237
- if (!Array.isArray(exportNames)) return;
250
+ }
251
+
252
+ function declarationsOf(headerFile, exportNames, dtsMode, log) {
238
253
  const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
239
- writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, emitCppDts(model, exportNames, dtsMode));
254
+ return emitCppDts(model, exportNames, dtsMode);
255
+ }
256
+
257
+ // Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
258
+ // user's header. Package headers are skipped: their types ship with the package itself, and a
259
+ // header staged from Conan is typed by its conan: module instead.
260
+ export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
261
+ const relative = path.relative(projectPath, headerFile);
262
+ if (relative.startsWith('..') || headerFile.startsWith(`${conanStageDir(cacheDir)}/`)) return;
263
+ const exportNames = readExportNames(exportsFile, log);
264
+ if (!exportNames) return;
265
+ writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, declarationsOf(headerFile, exportNames, dtsMode, log));
266
+ }
267
+
268
+ // A conan: specifier names no file the editor could map, so it is typed by an ambient module that
269
+ // @crossbind/typescript-config includes. `declare` is an error inside one (TS1038).
270
+ export function writeConanImportDts({ headerFile, exportsFile, declarationsFile = exportsFile, cacheDir, dtsMode = 'sync', log = () => {} }) {
271
+ const conanImport = conanImportOfHeader(headerFile, cacheDir);
272
+ const declared = conanImport && readExportNames(declarationsFile, log);
273
+ if (!declared) return;
274
+ // The bridge also binds macros and variadic functions the app imports, which the whole header's SWIG run skips.
275
+ const exportNames = [...new Set([...declared, ...(readExportNames(exportsFile, log) ?? [])])];
276
+ const body = declarationsOf(headerFile, exportNames, dtsMode, log)
277
+ .replaceAll('export declare ', 'export ')
278
+ .split('\n')
279
+ .map((line) => (line ? ` ${line}` : line))
280
+ .join('\n');
281
+ writeIfChanged(
282
+ `${conanStageDir(cacheDir)}/types/${conanImport.name}/${conanImport.header}.d.ts`,
283
+ `declare module 'conan:${conanImport.name}/${conanImport.header}' {\n${body}\n}\n`,
284
+ );
240
285
  }
@@ -0,0 +1,11 @@
1
+ // The tools xcode-select picks, behind /usr/bin's shims, which the Command Line Tools alone also provide,
2
+ // and the macOS every macOS archive is built for: port archives and Conan packages link into the same
3
+ // addon, so they move together.
4
+ export const DARWIN_TOOLS_BIN = '/usr/bin';
5
+ export const DARWIN_CC = `${DARWIN_TOOLS_BIN}/clang`;
6
+ export const DARWIN_CXX = `${DARWIN_TOOLS_BIN}/clang++`;
7
+ // Node 22, the oldest supported line, needs macOS 11.
8
+ export const DARWIN_DEPLOYMENT_TARGET = '11.0';
9
+ // Homebrew and MacPorts packages exist on the build machine only: an archive compiled against one fails
10
+ // to link, or to load, anywhere else.
11
+ export const DARWIN_HOST_PACKAGE_PREFIXES = ['/opt/homebrew', '/usr/local', '/opt/local'];