crossbind 0.0.1 → 2.0.0-beta.51
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 +92 -0
- package/CHANGELOG.md +156 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +52 -4
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildCargo.js +88 -0
- package/src/actions/buildDependencies.js +157 -0
- package/src/actions/buildExternal.js +57 -0
- package/src/actions/buildJs.js +93 -0
- package/src/actions/buildLib.js +115 -0
- package/src/actions/buildTypes.js +33 -0
- package/src/actions/buildWasiCommand.js +101 -0
- package/src/actions/buildWasm.js +322 -0
- package/src/actions/createInterface.js +172 -0
- package/src/actions/createLib.js +249 -0
- package/src/actions/createXCFramework.js +82 -0
- package/src/actions/extensions.js +7 -0
- package/src/actions/getAllBridges.js +20 -0
- package/src/actions/getCmakeParameters.js +66 -0
- package/src/actions/getData.js +46 -0
- package/src/actions/getDependLibs.js +25 -0
- package/src/actions/isSourceNewer.js +35 -0
- package/src/actions/licenses.js +156 -0
- package/src/actions/run.js +307 -0
- package/src/actions/target.js +54 -0
- package/src/assets/cmake/CMakeLists.txt +61 -0
- package/src/assets/cmake/dist.cmake +57 -0
- package/src/assets/cmake/ios.toolchain.cmake +1176 -0
- package/src/assets/cpp-runtime/browser.cpp +12 -0
- package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
- package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
- package/src/assets/cpp-runtime/node.cpp +19 -0
- package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
- package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
- package/src/assets/js-runtime/adapters/fs-node.js +28 -0
- package/src/assets/js-runtime/adapters/path-fs.js +6 -0
- package/src/assets/js-runtime/adapters/path-url.js +8 -0
- package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
- package/src/assets/js-runtime/browser.js +16 -0
- package/src/assets/js-runtime/core.js +193 -0
- package/src/assets/js-runtime/edge.js +18 -0
- package/src/assets/js-runtime/node.js +20 -0
- package/src/assets/packaging/crossbind-package.podspec +17 -0
- package/src/assets/wasi-runtime/stubs.c +84 -0
- package/src/bin.js +416 -0
- package/src/index.js +19 -0
- package/src/integration/getCrossbindScript.js +180 -0
- package/src/integration/getDependFilePath.js +132 -0
- package/src/runtime/wasiRun.mjs +118 -0
- package/src/state/calculateDependencyParameters.js +97 -0
- package/src/state/index.js +128 -0
- package/src/state/loadConfig.js +215 -0
- package/src/utils/appRustCrates.js +106 -0
- package/src/utils/binTools.js +76 -0
- package/src/utils/bridgeAsyncGuard.js +37 -0
- package/src/utils/cargoTarget.js +15 -0
- package/src/utils/cppDts.js +201 -0
- package/src/utils/cppFieldBindings.js +23 -0
- package/src/utils/dependencyRebuild.js +99 -0
- package/src/utils/dirLock.js +71 -0
- package/src/utils/downloadAndExtractFile.js +102 -0
- package/src/utils/familyManifest.js +17 -0
- package/src/utils/findFiles.js +5 -0
- package/src/utils/fixPackageName.js +7 -0
- package/src/utils/flattenConfigForTable.js +75 -0
- package/src/utils/getAbsolutePath.js +14 -0
- package/src/utils/getCMakeListsFilePath.js +17 -0
- package/src/utils/getOsUserAndGroupId.js +11 -0
- package/src/utils/getParentPath.js +12 -0
- package/src/utils/hash.js +33 -0
- package/src/utils/inputStamp.js +28 -0
- package/src/utils/licenseReport.js +94 -0
- package/src/utils/linkLayout.js +34 -0
- package/src/utils/loadJs.js +31 -0
- package/src/utils/loadJson.js +16 -0
- package/src/utils/logger.js +110 -0
- package/src/utils/ociArtifact.js +124 -0
- package/src/utils/overrideDependency.js +99 -0
- package/src/utils/provenance.js +54 -0
- package/src/utils/pullDockerImage.js +105 -0
- package/src/utils/replaceBasePathForDocker.js +18 -0
- package/src/utils/resolveEmbindRust.js +66 -0
- package/src/utils/runCargo.js +228 -0
- package/src/utils/rustBridgeGen.js +1013 -0
- package/src/utils/rustMt.js +76 -0
- package/src/utils/rustSysroot.js +171 -0
- package/src/utils/rustSysrootPin.js +11 -0
- package/src/utils/systemKeys.js +29 -0
- package/src/utils/targets.js +109 -0
- package/src/utils/wasiToolchain.js +47 -0
- package/src/utils/writeIfChanged.js +8 -0
- package/src/utils/writeJson.js +9 -0
- package/vitest.config.js +46 -0
package/src/bin.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { Command, Option } from 'commander';
|
|
6
|
+
|
|
7
|
+
import { state } from './index.js';
|
|
8
|
+
import createBridgeFile from './actions/createInterface.js';
|
|
9
|
+
import createLib from './actions/createLib.js';
|
|
10
|
+
import buildWasm from './actions/buildWasm.js';
|
|
11
|
+
import buildWasiCommand from './actions/buildWasiCommand.js';
|
|
12
|
+
import buildExternal from './actions/buildExternal.js';
|
|
13
|
+
import buildPackageTypes from './actions/buildTypes.js';
|
|
14
|
+
import buildLib from './actions/buildLib.js';
|
|
15
|
+
import buildDependencies from './actions/buildDependencies.js';
|
|
16
|
+
import runCrossbindApp from './actions/run.js';
|
|
17
|
+
import { getFilteredBuildTargets } from './actions/target.js';
|
|
18
|
+
|
|
19
|
+
import writeJson from './utils/writeJson.js';
|
|
20
|
+
import flattenConfigForTable from './utils/flattenConfigForTable.js';
|
|
21
|
+
import systemKeys from './utils/systemKeys.js';
|
|
22
|
+
import logger from './utils/logger.js';
|
|
23
|
+
import { getDockerImage, getDockerContainerName } from './utils/pullDockerImage.js';
|
|
24
|
+
import { cargoHome } from './utils/runCargo.js';
|
|
25
|
+
import { cleanDepsCache } from './utils/dependencyRebuild.js';
|
|
26
|
+
import collectLicenseRows from './actions/licenses.js';
|
|
27
|
+
import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
|
|
28
|
+
import findFiles from './utils/findFiles.js';
|
|
29
|
+
|
|
30
|
+
const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
|
31
|
+
|
|
32
|
+
const platforms = [...new Set(state.targets.map(target => target.platform).filter(t => t))];
|
|
33
|
+
const archs = [...new Set(state.targets.map(target => target.arch).filter(t => t))];
|
|
34
|
+
const runtimes = [...new Set(state.targets.map(target => target.runtime).filter(t => t))];
|
|
35
|
+
const buildTypes = [...new Set(state.targets.map(target => target.buildType).filter(t => t))];
|
|
36
|
+
const runtimeEnvs = [...new Set(state.targets.map(target => target.runtimeEnv).filter(t => t))];
|
|
37
|
+
|
|
38
|
+
// Build failures bubble up as exceptions with a `cause` chain (see actions/run.js);
|
|
39
|
+
// show the operator a single line — DEBUG=1 for the full stack — and exit non-zero.
|
|
40
|
+
const handleFatal = (e) => {
|
|
41
|
+
if (process.env.DEBUG) {
|
|
42
|
+
console.error(e);
|
|
43
|
+
} else {
|
|
44
|
+
let message = e?.message || String(e);
|
|
45
|
+
if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
|
|
46
|
+
console.error(message);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
};
|
|
50
|
+
process.on('uncaughtException', handleFatal);
|
|
51
|
+
process.on('unhandledRejection', handleFatal);
|
|
52
|
+
|
|
53
|
+
const program = new Command();
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.name('crossbind')
|
|
57
|
+
.description('Compile C++ files to WebAssembly and native platforms.')
|
|
58
|
+
.version(packageJSON.version)
|
|
59
|
+
.showHelpAfterError();
|
|
60
|
+
|
|
61
|
+
program.command('build')
|
|
62
|
+
.description('compile the project that was set up using crossbind')
|
|
63
|
+
.addOption(new Option('-p, --platform <platform>', 'target platform').argParser(createListParser(platforms)))
|
|
64
|
+
.addOption(new Option('-a, --arch <arch>', 'target architecture').argParser(createListParser(archs)))
|
|
65
|
+
.addOption(new Option('-r, --runtime <runtime>', 'target runtime').argParser(createListParser(runtimes)))
|
|
66
|
+
.addOption(new Option('-b, --build-type <buildType>', 'target build type').argParser(createListParser(buildTypes)))
|
|
67
|
+
.addOption(new Option('-e, --runtime-env <runtimeEnv>', 'target runtime environment').argParser(createListParser(runtimeEnvs)))
|
|
68
|
+
.option('--rebuild-deps [list]', 'rebuild dependencies from source instead of using prebuilt (all, or a comma-separated list of names)')
|
|
69
|
+
.action((options) => {
|
|
70
|
+
const { rebuildDeps, ...rest } = options;
|
|
71
|
+
const targetParams = { ...rest };
|
|
72
|
+
|
|
73
|
+
if (!targetParams.arch) {
|
|
74
|
+
targetParams.arch = archs.filter(item => item !== 'wasm64');
|
|
75
|
+
}
|
|
76
|
+
if (!targetParams.buildType) {
|
|
77
|
+
targetParams.buildType = ['release'];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
targetParams.platform = targetParams.platform || platforms;
|
|
81
|
+
targetParams.arch = targetParams.arch || archs;
|
|
82
|
+
targetParams.runtime = targetParams.runtime || runtimes;
|
|
83
|
+
targetParams.buildType = targetParams.buildType || buildTypes;
|
|
84
|
+
targetParams.runtimeEnv = targetParams.runtimeEnv || runtimeEnvs;
|
|
85
|
+
|
|
86
|
+
if (state.config.build.withBuildConfig) {
|
|
87
|
+
buildExternal(targetParams);
|
|
88
|
+
} else {
|
|
89
|
+
build(targetParams, rebuildDeps);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The path a package publishes its SBOM at, as recorded for the publish gate. Absent for ordinary
|
|
94
|
+
// projects, which keep the plain default.
|
|
95
|
+
function declaredSbomPath(projectDir) {
|
|
96
|
+
try {
|
|
97
|
+
const manifest = JSON.parse(fs.readFileSync(`${projectDir}/package.json`, 'utf8'));
|
|
98
|
+
return manifest.crossbind?.provenance?.sbom ?? null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
program.command('licenses')
|
|
105
|
+
.description('list bundled native dependencies with SPDX license, versions and source URL')
|
|
106
|
+
.option('--notices [file]', 'write a THIRD-PARTY-NOTICES markdown file (default: THIRD-PARTY-NOTICES.md)')
|
|
107
|
+
.option('--sbom [file]', 'write a CycloneDX SBOM json file (default: sbom.cdx.json)')
|
|
108
|
+
.option('--check', 'exit non-zero when a license field is missing or not valid SPDX')
|
|
109
|
+
.option('--platform <platform>', "also list what this platform's artifact statically links beyond the package graph (vendored copies, toolchain runtime)")
|
|
110
|
+
.action(async (options) => {
|
|
111
|
+
const rows = await collectLicenseRows(options.platform || null);
|
|
112
|
+
rows.forEach((row) => {
|
|
113
|
+
console.log(`${row.isCopyleft ? '! ' : ' '}${(row.name || '').padEnd(14)} ${String(row.license || '(missing)').padEnd(48)} native ${String(row.nativeVersion || '-').padEnd(10)} ${row.sourceUrl || ''}`);
|
|
114
|
+
});
|
|
115
|
+
const copyleftRows = rows.filter((row) => row.isCopyleft);
|
|
116
|
+
if (copyleftRows.length > 0) {
|
|
117
|
+
console.log(`\n! ${copyleftRows.length} copyleft-licensed ${copyleftRows.length === 1 ? 'dependency' : 'dependencies'} (${copyleftRows.map((row) => row.name).join(', ')}) — see docs/playbooks/licensing-lgpl.md`);
|
|
118
|
+
}
|
|
119
|
+
if (options.notices) {
|
|
120
|
+
const file = typeof options.notices === 'string' ? options.notices : `${state.config.paths.project}/THIRD-PARTY-NOTICES.md`;
|
|
121
|
+
fs.writeFileSync(file, formatNoticesMarkdown(rows));
|
|
122
|
+
logger.info(`crossbind: wrote ${file}`);
|
|
123
|
+
}
|
|
124
|
+
if (options.sbom) {
|
|
125
|
+
// Without a path, write where the package says its SBOM lives. A -bin package declares
|
|
126
|
+
// that in crossbind.provenance.sbom and the publish gate checks exactly that path, so
|
|
127
|
+
// taking it from there is what keeps `--sbom` from needing the path spelled out again
|
|
128
|
+
// in every package - and from disagreeing with the gate when one of them is edited.
|
|
129
|
+
const file = typeof options.sbom === 'string'
|
|
130
|
+
? options.sbom
|
|
131
|
+
: `${state.config.paths.project}/${declaredSbomPath(state.config.paths.project) ?? 'sbom.cdx.json'}`;
|
|
132
|
+
const target = { name: state.config.package?.name, version: state.config.package?.version };
|
|
133
|
+
fs.writeFileSync(file, formatCycloneDxSbom(rows, target));
|
|
134
|
+
logger.info(`crossbind: wrote ${file}`);
|
|
135
|
+
}
|
|
136
|
+
if (options.check) {
|
|
137
|
+
const invalid = rows
|
|
138
|
+
.map((row) => ({ row, result: validateSpdx(row.license) }))
|
|
139
|
+
.filter(({ result }) => !result.isValid);
|
|
140
|
+
invalid.forEach(({ row, result }) => console.error(`x ${row.name} (${row.npmName}): ${result.error}`));
|
|
141
|
+
if (invalid.length > 0) process.exit(1);
|
|
142
|
+
console.log('✓ all license fields are valid SPDX');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
program.command('clean-deps')
|
|
147
|
+
.description('remove the source-rebuilt dependency cache (all, or only the named dependencies)')
|
|
148
|
+
.argument('[names...]', 'dependency names (general name, package name or alias)')
|
|
149
|
+
.action((names) => {
|
|
150
|
+
const dirNames = names.length === 0 ? undefined : names.map((given) => {
|
|
151
|
+
const match = state.config.allDependencies.find((d) => [
|
|
152
|
+
d.general?.name, d.package?.name, d.general?.alias?.package,
|
|
153
|
+
].filter(Boolean).includes(given));
|
|
154
|
+
return match ? match.general.name : given;
|
|
155
|
+
});
|
|
156
|
+
const removed = cleanDepsCache(state.config.paths.cache, dirNames);
|
|
157
|
+
if (removed.length === 0) {
|
|
158
|
+
logger.info('crossbind: no rebuilt dependency cache to remove.');
|
|
159
|
+
} else {
|
|
160
|
+
logger.info(`crossbind: removed rebuilt cache for: ${removed.join(', ')}.`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// A container belongs to one image: the web container carries no NDK, and android runs a forced platform.
|
|
165
|
+
const IMAGE_ROLES = ['web', 'android'];
|
|
166
|
+
const dockerContainerName = (role) => {
|
|
167
|
+
if (!IMAGE_ROLES.includes(role)) {
|
|
168
|
+
console.error(`crossbind: unknown image "${role}" - expected one of ${IMAGE_ROLES.join(', ')}.`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
return getDockerContainerName(state.config.paths.base, role);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const dockerExec = (args) => {
|
|
175
|
+
try {
|
|
176
|
+
execFileSync('docker', args, { stdio: 'inherit' });
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error(`crossbind: docker command failed with exit code ${e?.status ?? 'unknown'}.`);
|
|
179
|
+
process.exit(e?.status || 1);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const commandDocker = program.command('docker').description('manage docker');
|
|
184
|
+
|
|
185
|
+
commandDocker.command('run')
|
|
186
|
+
.description('run docker application')
|
|
187
|
+
.argument('<program>', 'program to execute inside the container')
|
|
188
|
+
.argument('[params...]', 'arguments passed to the program')
|
|
189
|
+
.action((programName, params) => run(programName, params));
|
|
190
|
+
|
|
191
|
+
const imageArgument = (command) => command.argument('[image]', `image to act on (${IMAGE_ROLES.join(', ')})`, 'web');
|
|
192
|
+
|
|
193
|
+
imageArgument(commandDocker.command('create').description('create docker container'))
|
|
194
|
+
.action((image) => {
|
|
195
|
+
// Google ships the linux NDK for x86_64 only, so an android container is always amd64.
|
|
196
|
+
const platform = image === 'android' ? 'linux/amd64' : undefined;
|
|
197
|
+
dockerExec([
|
|
198
|
+
'run', '-dit',
|
|
199
|
+
...(platform ? ['--platform', platform] : []),
|
|
200
|
+
'--name', dockerContainerName(image),
|
|
201
|
+
'-v', `${state.config.paths.base}:/tmp/crossbind/live`,
|
|
202
|
+
// Cargo's registry cache, shared with host builds and with every other project.
|
|
203
|
+
'-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
|
|
204
|
+
getDockerImage(image, platform),
|
|
205
|
+
'bash',
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
imageArgument(commandDocker.command('start').description('start docker container'))
|
|
210
|
+
.action((image) => dockerExec(['start', dockerContainerName(image)]));
|
|
211
|
+
|
|
212
|
+
imageArgument(commandDocker.command('stop').description('stop docker container'))
|
|
213
|
+
.action((image) => dockerExec(['stop', dockerContainerName(image)]));
|
|
214
|
+
|
|
215
|
+
imageArgument(commandDocker.command('delete').description('delete docker container'))
|
|
216
|
+
.action((image) => dockerExec(['rm', dockerContainerName(image)]));
|
|
217
|
+
|
|
218
|
+
const commandConfig = program.command('config')
|
|
219
|
+
.description('manage the crossbind configuration files');
|
|
220
|
+
|
|
221
|
+
commandConfig.command('get')
|
|
222
|
+
.description('get the crossbind system configuration')
|
|
223
|
+
.argument('<key>', 'configuration key to read')
|
|
224
|
+
.action((key) => getSystemConfig(key));
|
|
225
|
+
|
|
226
|
+
commandConfig.command('set')
|
|
227
|
+
.description('set the crossbind system configuration')
|
|
228
|
+
.argument('<key>', 'configuration key to set')
|
|
229
|
+
.argument('<value>', 'value to assign')
|
|
230
|
+
.action((key, value) => setSystemConfig(key, value));
|
|
231
|
+
|
|
232
|
+
commandConfig.command('delete')
|
|
233
|
+
.description('delete the crossbind system configuration')
|
|
234
|
+
.argument('<key>', 'configuration key to remove')
|
|
235
|
+
.action((key) => deleteSystemConfig(key));
|
|
236
|
+
|
|
237
|
+
commandConfig.command('list')
|
|
238
|
+
.description('list the crossbind configurations')
|
|
239
|
+
.addOption(new Option('-t, --type <type>', 'config type').default('system').choices(['all', 'system', 'project']))
|
|
240
|
+
.action((options) => listSystemConfig(options.type));
|
|
241
|
+
|
|
242
|
+
commandConfig.command('keys')
|
|
243
|
+
.description('list all available system configuration keys for crossbind')
|
|
244
|
+
.action(() => listSystemKeys());
|
|
245
|
+
|
|
246
|
+
// `pnpm run <script> -- --flag` forwards the `--` literally and commander would then
|
|
247
|
+
// treat the flags as positionals. Strip the first separator for commands where every
|
|
248
|
+
// argument is an option; `run`/`docker run` keep it for program arguments.
|
|
249
|
+
const argv = [...process.argv];
|
|
250
|
+
if (['build', 'clean-deps', 'licenses'].includes(argv[2])) {
|
|
251
|
+
const separatorIndex = argv.indexOf('--');
|
|
252
|
+
if (separatorIndex !== -1) argv.splice(separatorIndex, 1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
program.parse(argv);
|
|
256
|
+
|
|
257
|
+
function listSystemKeys() {
|
|
258
|
+
console.info('Available configurations:');
|
|
259
|
+
console.table(systemKeys);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getSystemConfig(systemKey) {
|
|
263
|
+
if (!systemKey) {
|
|
264
|
+
listSystemConfig('system');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const key = systemKey.toUpperCase();
|
|
268
|
+
const output = {};
|
|
269
|
+
if (key in state.config.system) {
|
|
270
|
+
output[key] = { value: state.config.system[key] || 'undefined', default: systemKeys[key].default };
|
|
271
|
+
}
|
|
272
|
+
Object.keys(systemKeys).filter((k) => k.includes(key)).forEach((k) => {
|
|
273
|
+
if (k in state.config.system) {
|
|
274
|
+
output[k] = { value: state.config.system[k] || 'undefined', default: systemKeys[k].default };
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.table(output);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function setSystemConfig(key, value) {
|
|
282
|
+
if (!systemKeys[key]) {
|
|
283
|
+
throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
|
|
284
|
+
}
|
|
285
|
+
if (systemKeys[key].options && !systemKeys[key].options.includes(value)) {
|
|
286
|
+
throw new Error(`Value ${value} is not available. Please choose from the following available values: ${systemKeys[key].options.join(', ')}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
state.config.system[key] = value;
|
|
290
|
+
writeJson(state.config.paths.systemConfig, state.config.system);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function deleteSystemConfig(key) {
|
|
294
|
+
if (!systemKeys[key]) {
|
|
295
|
+
throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
delete state.config.system[key];
|
|
298
|
+
writeJson(state.config.paths.systemConfig, state.config.system);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function listSystemConfig(type) {
|
|
302
|
+
const { system: systemConfig, ...projectConfig } = state.config;
|
|
303
|
+
if (type === 'all' || type === 'system') {
|
|
304
|
+
console.log('System Configuration');
|
|
305
|
+
console.table(systemConfig);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (type === 'all') {
|
|
309
|
+
console.log('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (type === 'all' || type === 'project') {
|
|
313
|
+
console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
|
|
314
|
+
console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function run(programName, params) {
|
|
319
|
+
runCrossbindApp(programName, params, null, null, { console: true });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function build(targetParams, rebuildOption) {
|
|
323
|
+
await buildDependencies({ targetParams, rebuildOption });
|
|
324
|
+
buildLib(targetParams);
|
|
325
|
+
createWasmJs(targetParams);
|
|
326
|
+
await createWasiCommands(targetParams);
|
|
327
|
+
buildPackageTypes();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function createWasiCommands(targetParams) {
|
|
331
|
+
const targets = getFilteredBuildTargets(targetParams, { platform: 'wasi' });
|
|
332
|
+
if (targets.length === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (const target of targets) {
|
|
336
|
+
const distWasm = `${state.config.paths.output}/${target.wasmName}`;
|
|
337
|
+
const built = await buildWasiCommand(target, { force: !fs.existsSync(distWasm) });
|
|
338
|
+
if (!built) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.wasmName}`, distWasm);
|
|
342
|
+
// Copied (not renamed) so a later fingerprint cache-hit still has the
|
|
343
|
+
// build-dir tree to serve from.
|
|
344
|
+
if (fs.existsSync(`${state.config.paths.build}/data`)) {
|
|
345
|
+
fs.rmSync(`${state.config.paths.output}/data`, { recursive: true, force: true });
|
|
346
|
+
fs.cpSync(`${state.config.paths.build}/data`, `${state.config.paths.output}/data`, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function createWasmJs(targetParams) {
|
|
352
|
+
if (state.config.export.bundle === false) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
|
|
356
|
+
if (targets.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let headers = [];
|
|
360
|
+
state.config.paths.header.forEach((headerPath) => {
|
|
361
|
+
headers.push(findFiles('**/*.h', { cwd: headerPath }));
|
|
362
|
+
});
|
|
363
|
+
headers = headers.filter((path) => !!path.toString()).flat();
|
|
364
|
+
|
|
365
|
+
const bridges = [];
|
|
366
|
+
headers.forEach((header) => {
|
|
367
|
+
const bridgePath = createBridgeFile(header);
|
|
368
|
+
bridges.push(bridgePath);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const opt = {
|
|
372
|
+
buildSource: false,
|
|
373
|
+
nativeGlob: [
|
|
374
|
+
`${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`,
|
|
375
|
+
...bridges,
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
for (const target of targets) {
|
|
380
|
+
// An artifact merely existing in dist used to short-circuit this whole
|
|
381
|
+
// step, which kept serving stale output after flag/runtime changes.
|
|
382
|
+
// Let the link fingerprint inside buildWasm decide instead; force the
|
|
383
|
+
// relink when dist is incomplete, because a fingerprint hit skips the
|
|
384
|
+
// link and the copy step below moved build/data away on the last run.
|
|
385
|
+
const distComplete = fs.existsSync(`${state.config.paths.output}/${target.jsName}`)
|
|
386
|
+
&& fs.existsSync(`${state.config.paths.output}/${target.wasmName}`);
|
|
387
|
+
createLib(target, 'Bridge', opt);
|
|
388
|
+
const built = await buildWasm(target, { force: !distComplete });
|
|
389
|
+
if (!built) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fs.rmSync(`${state.config.paths.output}/data`, { recursive: true, force: true });
|
|
394
|
+
if (fs.existsSync(`${state.config.paths.build}/data`)) {
|
|
395
|
+
fs.renameSync(`${state.config.paths.build}/data`, `${state.config.paths.output}/data`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.jsName}`, `${state.config.paths.output}/${target.jsName}`);
|
|
399
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.wasmName}`, `${state.config.paths.output}/${target.wasmName}`);
|
|
400
|
+
if (fs.existsSync(`${state.config.paths.build}/${target.dataTxtName}`)) {
|
|
401
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.dataTxtName}`, `${state.config.paths.output}/${target.dataTxtName}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createListParser(validList) {
|
|
407
|
+
return (value, previous) => {
|
|
408
|
+
const items = value.split(',').map(item => item.trim());
|
|
409
|
+
for (const item of items) {
|
|
410
|
+
if (!validList.includes(item)) {
|
|
411
|
+
throw new Error(`Invalid value: "${item}". Allowed values: ${validList.join(', ')}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return previous ? previous.concat(items) : items;
|
|
415
|
+
};
|
|
416
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { default as state } from './state/index.js';
|
|
2
|
+
export { default as getParentPath } from './utils/getParentPath.js';
|
|
3
|
+
export { default as createLib } from './actions/createLib.js';
|
|
4
|
+
export { default as createBridgeFile } from './actions/createInterface.js';
|
|
5
|
+
export { default as buildWasm } from './actions/buildWasm.js';
|
|
6
|
+
export { default as isSourceNewer } from './actions/isSourceNewer.js';
|
|
7
|
+
export { default as getData } from './actions/getData.js';
|
|
8
|
+
export { default as getCmakeParameters } from './actions/getCmakeParameters.js';
|
|
9
|
+
export { default as createXCFramework } from './actions/createXCFramework.js';
|
|
10
|
+
export { default as getAllBridges } from './actions/getAllBridges.js';
|
|
11
|
+
export { default as run } from './actions/run.js';
|
|
12
|
+
export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredTargetSpec } from './actions/target.js';
|
|
13
|
+
export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
|
|
14
|
+
export { default as getDependFilePath } from './integration/getDependFilePath.js';
|
|
15
|
+
export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
|
|
16
|
+
export { default as findFiles } from './utils/findFiles.js';
|
|
17
|
+
export { getContentHash, getFileHash } from './utils/hash.js';
|
|
18
|
+
export { collectInputFiles, computeInputStamp } from './utils/inputStamp.js';
|
|
19
|
+
export { default as buildAppRustCrates } from './utils/appRustCrates.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import getData from '../actions/getData.js';
|
|
4
|
+
import loadJson from '../utils/loadJson.js';
|
|
5
|
+
import state from '../state/index.js';
|
|
6
|
+
import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
|
|
7
|
+
|
|
8
|
+
export default function getCrossbindScript(target, bridgePath) {
|
|
9
|
+
if (!bridgePath) {
|
|
10
|
+
throw new Error('getCrossbindScript needs the bridge file of the imported header');
|
|
11
|
+
}
|
|
12
|
+
return buildScript(target, loadJson(`${bridgePath}.exports.json`));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The Rust analog of a .h import: parse the crate surface and emit the same proxy module
|
|
16
|
+
// (per-symbol lets assigned inside this module's initNative). Vectors come from the owning
|
|
17
|
+
// cargo package's config; classes/enums come from the parsed source.
|
|
18
|
+
export function getRustJsScript(target, rsFile) {
|
|
19
|
+
// Compare real paths: dependency paths go through node_modules symlinks (pnpm workspaces)
|
|
20
|
+
// while bundlers hand the transformer the resolved real file.
|
|
21
|
+
const realCrateDir = (d) => {
|
|
22
|
+
try { return fs.realpathSync(path.resolve(d.paths.project, d.export.crate ?? 'crate')); } catch (e) { return null; }
|
|
23
|
+
};
|
|
24
|
+
const rsReal = fs.realpathSync(rsFile);
|
|
25
|
+
|
|
26
|
+
// Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
|
|
27
|
+
// upstream crate's own source, and the bridge is synthesized against the cargo dependency.
|
|
28
|
+
const crateName = path.basename(rsReal, '.rs');
|
|
29
|
+
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
30
|
+
if (Object.hasOwn(cargoDeps, crateName)
|
|
31
|
+
&& path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
|
|
32
|
+
const { model } = createCrateImportBridge({
|
|
33
|
+
crateName,
|
|
34
|
+
spec: cargoDeps[crateName],
|
|
35
|
+
cacheDir: state.config.paths.cache,
|
|
36
|
+
dtsMode: state.config.dts,
|
|
37
|
+
log: () => {},
|
|
38
|
+
});
|
|
39
|
+
return buildScript(target, [
|
|
40
|
+
...model.classes.map((c) => c.name),
|
|
41
|
+
...model.enums.map((e) => e.name),
|
|
42
|
+
...model.freeFns.map((f) => f.jsName),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pkg = state.config.allDependencies.find((d) => d.export?.type === 'cargo'
|
|
47
|
+
&& realCrateDir(d) && rsReal.startsWith(`${realCrateDir(d)}${path.sep}`));
|
|
48
|
+
const model = parseSurface(fs.readFileSync(rsFile, 'utf8'), () => {});
|
|
49
|
+
const vectors = pkg ? (pkg.export?.bindings?.vectors ?? []) : (state.config.export?.bindings?.vectors ?? []);
|
|
50
|
+
if (!pkg) {
|
|
51
|
+
// App-local .rs: synthesize its bridge crate on import (the C++ createBridgeFile analog);
|
|
52
|
+
// the native builds pick every crate under .crossbind/rust-bridges up and link it.
|
|
53
|
+
createRustBridgeCrate({
|
|
54
|
+
rsFile: rsReal,
|
|
55
|
+
cacheDir: state.config.paths.cache,
|
|
56
|
+
projectPath: state.config.paths.project,
|
|
57
|
+
dtsMode: state.config.dts,
|
|
58
|
+
vectors,
|
|
59
|
+
cargoDependencies: state.config.cargoDependencies ?? {},
|
|
60
|
+
log: () => {},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const symbols = [
|
|
64
|
+
...model.classes.map((c) => c.name),
|
|
65
|
+
...model.enums.map((e) => e.name),
|
|
66
|
+
...(model.freeFns ?? []).map((f) => f.jsName),
|
|
67
|
+
...vectors.map((v) => v.name),
|
|
68
|
+
];
|
|
69
|
+
return buildScript(target, symbols);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildScript(target, symbols) {
|
|
73
|
+
if (!target) {
|
|
74
|
+
throw new Error('The target is not available!');
|
|
75
|
+
}
|
|
76
|
+
const env = JSON.stringify(getData('env', target));
|
|
77
|
+
const getPlatformScript = target.platform === 'wasm' ? getWebScript : getReactNativeScript;
|
|
78
|
+
|
|
79
|
+
let symbolExportDefineString = '';
|
|
80
|
+
let symbolExportAssignString = '';
|
|
81
|
+
if (symbols && Array.isArray(symbols)) {
|
|
82
|
+
symbolExportDefineString = symbols.map((s) => `export let ${s} = null;`).join('\n');
|
|
83
|
+
symbolExportAssignString = symbols.map((s) => `${s} = m.${s};`).join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `
|
|
87
|
+
${getPlatformScript(env)}
|
|
88
|
+
|
|
89
|
+
export let AllSymbols = {};
|
|
90
|
+
${symbolExportDefineString}
|
|
91
|
+
|
|
92
|
+
// Each proxy module registers how to bind its own exports, so one init() - from 'crossbind'
|
|
93
|
+
// or from any single module - resolves every imported module. Binding only inside the
|
|
94
|
+
// owning module's init is what leaves the others' exports null.
|
|
95
|
+
function __crossbindBind(m) {
|
|
96
|
+
AllSymbols = m;
|
|
97
|
+
${symbolExportAssignString}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!globalThis.__crossbindBinders) globalThis.__crossbindBinders = new Set();
|
|
101
|
+
globalThis.__crossbindBinders.add(__crossbindBind);
|
|
102
|
+
// Imported after the runtime booted: this registration missed the run, so bind now.
|
|
103
|
+
if (globalThis.__crossbindModule) __crossbindBind(globalThis.__crossbindModule);
|
|
104
|
+
|
|
105
|
+
function __crossbindInit(config = {}) {
|
|
106
|
+
// Boot once per app: the wasm module (or the JSI lib) must only start once.
|
|
107
|
+
if (!globalThis.__crossbindBootPromise) {
|
|
108
|
+
globalThis.__crossbindBootPromise = __crossbindBoot(config);
|
|
109
|
+
}
|
|
110
|
+
return globalThis.__crossbindBootPromise.then((m) => {
|
|
111
|
+
globalThis.__crossbindModule = m;
|
|
112
|
+
globalThis.__crossbindBinders.forEach((bind) => bind(m));
|
|
113
|
+
return m;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Dropping the boot promise here is what lets a later init() start a fresh runtime;
|
|
118
|
+
// the bound exports stay pointed at the dead module until it does.
|
|
119
|
+
__crossbindInit.terminate = function terminate() {
|
|
120
|
+
globalThis.__crossbindBootPromise = null;
|
|
121
|
+
globalThis.__crossbindModule = null;
|
|
122
|
+
if (globalThis.Crossbind && globalThis.Crossbind.initNative.terminate) {
|
|
123
|
+
globalThis.Crossbind.initNative.terminate();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Every generated module owns initNative(): one call boots the runtime and binds
|
|
128
|
+
// every imported module, so the app calls it from whichever import it already has.
|
|
129
|
+
export { __crossbindInit as initNative };
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getReactNativeScript(env) {
|
|
134
|
+
return `
|
|
135
|
+
import { NativeModules } from 'react-native';
|
|
136
|
+
import Module from '@crossbind/core-embind-jsi';
|
|
137
|
+
|
|
138
|
+
const { RNJsiLib } = NativeModules;
|
|
139
|
+
|
|
140
|
+
function setEnv() {
|
|
141
|
+
const env = JSON.parse('${env}');
|
|
142
|
+
const CROSSBIND_DATA_PATH = Module.Crossbind.getEnv('CROSSBIND_DATA_PATH');
|
|
143
|
+
|
|
144
|
+
Object.entries(env).forEach(([key, value]) => {
|
|
145
|
+
Module.Crossbind.setEnv(key, value.replace('_CROSSBIND_DATA_PATH_', CROSSBIND_DATA_PATH), false);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Starting the JSI lib twice re-runs every embind registration and aborts with
|
|
150
|
+
// "Cannot register public name ... twice", so __crossbindInit keeps this to one call.
|
|
151
|
+
async function __crossbindBoot() {
|
|
152
|
+
if (!RNJsiLib || !RNJsiLib.start) {
|
|
153
|
+
throw new Error('Module failed to initialise.');
|
|
154
|
+
}
|
|
155
|
+
await RNJsiLib.start();
|
|
156
|
+
setEnv();
|
|
157
|
+
return Module;
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getWebScript(env) {
|
|
163
|
+
const params = `{
|
|
164
|
+
...config,
|
|
165
|
+
env: {...${env}, ...config.env},
|
|
166
|
+
paths: {
|
|
167
|
+
wasm: 'crossbind.wasm',
|
|
168
|
+
data: 'crossbind.data.txt',
|
|
169
|
+
worker: 'crossbind.js',
|
|
170
|
+
js: 'crossbind.js',
|
|
171
|
+
}
|
|
172
|
+
}`;
|
|
173
|
+
|
|
174
|
+
return `
|
|
175
|
+
function __crossbindBoot(config) {
|
|
176
|
+
return import(/* webpackIgnore: true */ '/crossbind.js')
|
|
177
|
+
.then(n => window.Crossbind.initNative(${params}));
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
}
|