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.
- package/README.md +8 -6
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +37 -9
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +27 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +190 -0
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +92 -95
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +318 -65
- package/src/actions/createLib.js +60 -43
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +70 -0
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +175 -17
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +124 -32
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +27 -13
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +290 -37
- package/src/index.js +6 -1
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +53 -19
- package/src/integration/getDependFilePath.js +18 -12
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +31 -20
- package/src/state/loadConfig.js +36 -24
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +17 -1
- package/src/utils/appSources.js +112 -0
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +23 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +134 -55
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +84 -0
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +56 -0
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/inputStamp.js +23 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linkLayout.js +8 -1
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +101 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +10 -5
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +51 -53
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +903 -253
- package/src/utils/rustMt.js +4 -0
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +86 -6
- package/src/utils/systemKeys.js +43 -3
- package/src/utils/targets.js +104 -3
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
- 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
|
+
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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() === '') {
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function declarationsOf(headerFile, exportNames, dtsMode, log) {
|
|
202
251
|
const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
|
|
203
|
-
|
|
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'];
|