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
@@ -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
+ }
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { stripComments } from './swigInterface.js';
3
4
  import writeIfChanged from './writeIfChanged.js';
5
+ import { conanImportOfHeader, conanStageDir } from './conanImport.js';
4
6
 
5
7
  // Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
6
8
  // binding-rules surface (classes, public methods, primitives/string/shared_ptr), skip
@@ -33,7 +35,7 @@ function tsType(raw, classNames, { isReturn = false } = {}) {
33
35
  if (t === 'void') return 'void';
34
36
  if (t === 'bool') return 'boolean';
35
37
  if (NUMBER_TYPES.has(t)) return 'number';
36
- if (t === 'std::string') return 'string';
38
+ if (t === 'std::string' || t === 'std::u16string') return 'string';
37
39
  if (classNames.has(t)) return t;
38
40
  return null;
39
41
  }
@@ -41,16 +43,7 @@ function tsType(raw, classNames, { isReturn = false } = {}) {
41
43
  function parseArgs(rawArgs, classNames) {
42
44
  const trimmed = rawArgs.trim();
43
45
  if (trimmed === '' || trimmed === 'void') return [];
44
- const parts = [];
45
- let depth = 0;
46
- let current = '';
47
- for (const ch of trimmed) {
48
- if (ch === '<' || ch === '(') depth += 1;
49
- if (ch === '>' || ch === ')') depth -= 1;
50
- if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else current += ch;
51
- }
52
- parts.push(current);
53
- return parts.map((part, i) => {
46
+ return splitTopLevel(trimmed).map((part, i) => {
54
47
  const noDefault = part.split('=')[0].trim();
55
48
  // Declaration-style unnamed parameter: the whole token is a type.
56
49
  const unnamed = tsType(noDefault, classNames);
@@ -81,68 +74,126 @@ function bodyStatements(body) {
81
74
  return statements.map((s) => s.trim()).filter(Boolean);
82
75
  }
83
76
 
77
+ // Splits at commas outside <>, (), [] and {}.
78
+ function splitTopLevel(text) {
79
+ const parts = [];
80
+ let depth = 0;
81
+ let current = '';
82
+ for (const ch of text) {
83
+ if ('<([{'.includes(ch)) depth += 1;
84
+ if ('>)]}'.includes(ch)) depth -= 1;
85
+ if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else current += ch;
86
+ }
87
+ return [...parts, current];
88
+ }
89
+
90
+ // Value-semantics fields only: these are the types embind's .property can expose
91
+ // without ownership questions (and the worker clone can carry).
92
+ const FIELD_TYPES = new Set(['number', 'boolean', 'string']);
93
+
94
+ // `int x, *p, y = 2;` declares the int fields x and y and the pointer p. The types cover the value fields: pointers,
95
+ // arrays, bit-fields and references are left out.
96
+ const FIRST_DECLARATOR = /^([\s\S]*?)([*&\s]*)\b([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*|:\s*\w+)$/;
97
+ const NEXT_DECLARATOR = /^([*&\s]*)([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*|:\s*\w+)$/;
98
+
99
+ function valueField(typeText, classNames) {
100
+ const type = tsType(typeText.replace(/\b(?:const|volatile)\b/g, '').replace(/\s+/g, ' ').trim(), classNames);
101
+ return FIELD_TYPES.has(type) ? { type } : null;
102
+ }
103
+
104
+ function declaredFields(statement, classNames) {
105
+ const [first, ...rest] = splitTopLevel(statement).map((part) => part.split('=')[0].trim());
106
+ const head = first.match(FIRST_DECLARATOR);
107
+ if (!head || !head[1].trim()) return [];
108
+ const value = valueField(head[1], classNames);
109
+ const declarators = [head.slice(2), ...rest.map((part) => part.match(NEXT_DECLARATOR)?.slice(1) ?? null)];
110
+ return declarators.flatMap((declarator) => {
111
+ if (!value || !declarator || declarator[2] || /[*&]/.test(declarator[0])) return [];
112
+ return [{ name: declarator[1], ...value }];
113
+ });
114
+ }
115
+
116
+ // `struct Name {`, `class Name : Base {` and `typedef struct [Tag] { ... } Alias, *Pointer;`, unions too.
117
+ // A class takes its tag, or its first typedef name when it has none, and keeps its other typedef names:
118
+ // SWIG registers a typedef'd struct under the typedef name.
119
+ const CLASS_HEAD = /\b(typedef\s+)?(class|struct|union)\b\s*([A-Za-z_]\w*)?\s*(?::[^{;]*)?\{/g;
120
+
121
+ function closingBrace(text, from) {
122
+ let depth = 1;
123
+ let i = from;
124
+ while (i < text.length && depth > 0) {
125
+ if (text[i] === '{') depth += 1;
126
+ if (text[i] === '}') depth -= 1;
127
+ i += 1;
128
+ }
129
+ return i;
130
+ }
131
+
132
+ function typedefNamesAfter(text, from) {
133
+ const tail = text.slice(from).match(/^([^;{}]*);/);
134
+ return tail ? tail[1].split(',').map((part) => part.trim()).filter((part) => /^[A-Za-z_]\w*$/.test(part)) : [];
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
+
84
141
  export function parseCppSurface(source, log = console.log) {
85
- const clean = source
86
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
87
- .replace(/\/\/[^\n]*/g, ' ')
88
- .replace(/^[ \t]*#[^\n]*$/gm, ' ');
142
+ const clean = blankLiterals(stripComments(source)).replace(/^[ \t]*#[^\n]*$/gm, ' ');
89
143
 
90
144
  const classes = [];
91
145
  const classNames = new Set();
92
- const classRe = /\b(class|struct)\s+([A-Za-z_]\w*)\s*(?::[^{]*)?\{/g;
93
146
  const found = [];
94
- let m = classRe.exec(clean);
95
- while (m) {
96
- let depth = 1;
97
- let i = classRe.lastIndex;
98
- while (i < clean.length && depth > 0) {
99
- if (clean[i] === '{') depth += 1;
100
- if (clean[i] === '}') depth -= 1;
101
- i += 1;
102
- }
103
- found.push({ kind: m[1], name: m[2], body: clean.slice(classRe.lastIndex, i - 1) });
104
- classNames.add(m[2]);
105
- m = classRe.exec(clean);
147
+ for (const m of clean.matchAll(CLASS_HEAD)) {
148
+ const [head, typedefKeyword, kind, tag] = m;
149
+ const bodyStart = m.index + head.length;
150
+ const end = closingBrace(clean, bodyStart);
151
+ const names = typedefKeyword ? typedefNamesAfter(clean, end) : [];
152
+ const name = tag ?? names[0];
153
+ if (!name) continue;
154
+ found.push({ kind, name, aliases: names.filter((alias) => alias !== name), body: clean.slice(bodyStart, end - 1) });
155
+ classNames.add(name);
156
+ names.forEach((alias) => classNames.add(alias));
106
157
  }
107
158
 
108
- // Value-semantics fields only: these are the types embind's .property can expose
109
- // without ownership questions (and the worker clone can carry).
110
- const FIELD_TYPES = new Set(['number', 'boolean', 'string']);
111
-
112
159
  for (const cls of found) {
113
- let access = cls.kind === 'struct' ? 'public' : 'private';
160
+ let access = cls.kind === 'class' ? 'private' : 'public';
114
161
  let ctor = null;
162
+ let declaresCtor = false;
163
+ let isAbstract = false;
115
164
  const methods = [];
116
165
  const fields = [];
117
166
  for (let statement of bodyStatements(cls.body)) {
118
167
  const sections = statement.match(/\b(public|private|protected)\s*:\s*([\s\S]*)$/);
119
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;
120
171
  if (!statement || access !== 'public') continue;
121
- 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();
122
174
 
123
175
  const sig = statement.match(/^(static\s+)?(?:explicit\s+)?([\w:<>,\s*&]*?)\s*\b([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/);
124
176
  if (!sig) {
125
- const decl = statement.split('=')[0].trim();
126
- const field = !statement.includes('(') && !/^(static|using|typedef|friend|enum)\b/.test(decl)
127
- ? decl.match(/^(.*?)\b([A-Za-z_]\w*)$/s)
128
- : null;
129
- if (field) {
130
- const type = tsType(field[1], classNames);
131
- // A reference member has no address a pointer to member could name, and a pointer member is memory.
132
- if (type !== null && FIELD_TYPES.has(type) && !/[&*]/.test(field[1])) fields.push({ name: field[2], type });
177
+ if (!statement.includes('(') && !/^(static|using|typedef|friend|enum)\b/.test(statement)) {
178
+ fields.push(...declaredFields(statement, classNames));
133
179
  }
134
180
  continue;
135
181
  }
136
182
  const [, staticKw, retRaw, name, argsRaw] = sig;
137
183
  const args = parseArgs(argsRaw, classNames);
138
184
  if (args.includes(null)) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported parameter type)`); continue; }
139
- if (name === cls.name && retRaw.trim() === '') { ctor = { args }; continue; }
185
+ if (name === cls.name && retRaw.trim() === '') {
186
+ if (!isDeleted) ctor = { args };
187
+ continue;
188
+ }
140
189
  if (name.startsWith('~')) continue;
141
190
  const ret = tsType(retRaw, classNames, { isReturn: true });
142
191
  if (ret === null) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported return type '${retRaw.trim()}')`); continue; }
143
192
  methods.push({ name, isStatic: Boolean(staticKw), args, ret });
144
193
  }
145
- classes.push({ name: cls.name, ctor, methods, fields });
194
+ // The bridge binds the implicit default constructor of a class that declares none, unless it is abstract.
195
+ if (!declaresCtor && !isAbstract) ctor = { args: [] };
196
+ classes.push({ name: cls.name, aliases: cls.aliases, ctor, methods, fields });
146
197
  }
147
198
  return { classes };
148
199
  }
@@ -185,20 +236,48 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
185
236
  return out.join('\n');
186
237
  }
187
238
 
188
- // Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
189
- // user's header. Package headers are skipped: their types ship with the package itself.
190
- export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
191
- if (!fs.existsSync(exportsFile)) return;
192
- const relative = path.relative(projectPath, headerFile);
193
- if (relative.startsWith('..')) return;
194
- let exportNames;
239
+ function readExportNames(exportsFile, log) {
240
+ if (!fs.existsSync(exportsFile)) return null;
195
241
  try {
196
- exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
242
+ const exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
243
+ return Array.isArray(exportNames) ? exportNames : null;
197
244
  } catch (e) {
198
245
  log(`crossbind: dts: unreadable exports file ${exportsFile} (${e.message})`);
199
- return;
246
+ return null;
200
247
  }
201
- if (!Array.isArray(exportNames)) return;
248
+ }
249
+
250
+ function declarationsOf(headerFile, exportNames, dtsMode, log) {
202
251
  const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
203
- writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, emitCppDts(model, exportNames, dtsMode));
252
+ return emitCppDts(model, exportNames, dtsMode);
253
+ }
254
+
255
+ // Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
256
+ // user's header. Package headers are skipped: their types ship with the package itself, and a
257
+ // header staged from Conan is typed by its conan: module instead.
258
+ export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
259
+ const relative = path.relative(projectPath, headerFile);
260
+ if (relative.startsWith('..') || headerFile.startsWith(`${conanStageDir(cacheDir)}/`)) return;
261
+ const exportNames = readExportNames(exportsFile, log);
262
+ if (!exportNames) return;
263
+ writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, declarationsOf(headerFile, exportNames, dtsMode, log));
264
+ }
265
+
266
+ // A conan: specifier names no file the editor could map, so it is typed by an ambient module that
267
+ // @crossbind/typescript-config includes. `declare` is an error inside one (TS1038).
268
+ export function writeConanImportDts({ headerFile, exportsFile, declarationsFile = exportsFile, cacheDir, dtsMode = 'sync', log = () => {} }) {
269
+ const conanImport = conanImportOfHeader(headerFile, cacheDir);
270
+ const declared = conanImport && readExportNames(declarationsFile, log);
271
+ if (!declared) return;
272
+ // The bridge also binds macros and variadic functions the app imports, which the whole header's SWIG run skips.
273
+ const exportNames = [...new Set([...declared, ...(readExportNames(exportsFile, log) ?? [])])];
274
+ const body = declarationsOf(headerFile, exportNames, dtsMode, log)
275
+ .replaceAll('export declare ', 'export ')
276
+ .split('\n')
277
+ .map((line) => (line ? ` ${line}` : line))
278
+ .join('\n');
279
+ writeIfChanged(
280
+ `${conanStageDir(cacheDir)}/types/${conanImport.name}/${conanImport.header}.d.ts`,
281
+ `declare module 'conan:${conanImport.name}/${conanImport.header}' {\n${body}\n}\n`,
282
+ );
204
283
  }
@@ -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'];