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.
- package/AGENTS.md +2 -1
- package/README.md +2 -2
- package/package.json +7 -2
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +49 -10
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +10 -9
- package/src/actions/buildWasm.js +72 -31
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +71 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +70 -42
- package/src/actions/getData.js +6 -2
- package/src/actions/getEnv.js +7 -0
- package/src/actions/getLinkInputs.js +12 -5
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +143 -19
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +63 -66
- package/src/actions/runnerCommands.js +433 -0
- package/src/actions/writeRuntimeEntry.js +80 -0
- 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/cpp-runtime/node.cpp +18 -1
- 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 +138 -60
- package/src/assets/js-runtime/core.js +12 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +218 -161
- package/src/cli.js +14 -0
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +50 -13
- package/src/integration/getDependFilePath.js +9 -0
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/program.js +169 -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 +20 -11
- package/src/state/loadConfig.js +37 -35
- 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 +19 -1
- package/src/utils/appSources.js +97 -6
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +554 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cargoTarget.js +21 -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 +60 -15
- 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 +41 -2
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +16 -5
- package/src/utils/hostPlatform.js +21 -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/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 +103 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +3 -1
- 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/resetCMakeCacheForSource.js +26 -0
- package/src/utils/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +22 -8
- package/src/utils/rustMt.js +9 -0
- package/src/utils/rustSysroot.js +7 -5
- 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 +70 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +60 -4
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiHttp.js +50 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- 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
|
+
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -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
|
-
|
|
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() === '') {
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function declarationsOf(headerFile, exportNames, dtsMode, log) {
|
|
238
253
|
const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
|
|
239
|
-
|
|
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'];
|