crossbind 0.0.1 → 2.0.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/AGENTS.md +92 -0
  2. package/CHANGELOG.md +145 -0
  3. package/LICENSE +21 -0
  4. package/README.md +133 -0
  5. package/package.json +52 -4
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildCargo.js +88 -0
  8. package/src/actions/buildDependencies.js +157 -0
  9. package/src/actions/buildExternal.js +57 -0
  10. package/src/actions/buildJs.js +93 -0
  11. package/src/actions/buildLib.js +115 -0
  12. package/src/actions/buildTypes.js +33 -0
  13. package/src/actions/buildWasiCommand.js +101 -0
  14. package/src/actions/buildWasm.js +322 -0
  15. package/src/actions/createInterface.js +172 -0
  16. package/src/actions/createLib.js +249 -0
  17. package/src/actions/createXCFramework.js +82 -0
  18. package/src/actions/extensions.js +7 -0
  19. package/src/actions/getAllBridges.js +20 -0
  20. package/src/actions/getCmakeParameters.js +66 -0
  21. package/src/actions/getData.js +46 -0
  22. package/src/actions/getDependLibs.js +25 -0
  23. package/src/actions/isSourceNewer.js +35 -0
  24. package/src/actions/licenses.js +156 -0
  25. package/src/actions/run.js +305 -0
  26. package/src/actions/target.js +54 -0
  27. package/src/assets/cmake/CMakeLists.txt +61 -0
  28. package/src/assets/cmake/dist.cmake +57 -0
  29. package/src/assets/cmake/ios.toolchain.cmake +1176 -0
  30. package/src/assets/cpp-runtime/browser.cpp +12 -0
  31. package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
  32. package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
  33. package/src/assets/cpp-runtime/node.cpp +19 -0
  34. package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
  35. package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
  36. package/src/assets/js-runtime/adapters/fs-node.js +28 -0
  37. package/src/assets/js-runtime/adapters/path-fs.js +6 -0
  38. package/src/assets/js-runtime/adapters/path-url.js +8 -0
  39. package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
  40. package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
  41. package/src/assets/js-runtime/browser.js +16 -0
  42. package/src/assets/js-runtime/core.js +193 -0
  43. package/src/assets/js-runtime/edge.js +18 -0
  44. package/src/assets/js-runtime/node.js +20 -0
  45. package/src/assets/packaging/crossbind-package.podspec +17 -0
  46. package/src/assets/wasi-runtime/stubs.c +84 -0
  47. package/src/bin.js +416 -0
  48. package/src/index.js +19 -0
  49. package/src/integration/getCrossbindScript.js +180 -0
  50. package/src/integration/getDependFilePath.js +132 -0
  51. package/src/runtime/wasiRun.mjs +118 -0
  52. package/src/state/calculateDependencyParameters.js +97 -0
  53. package/src/state/index.js +128 -0
  54. package/src/state/loadConfig.js +215 -0
  55. package/src/utils/appRustCrates.js +106 -0
  56. package/src/utils/binTools.js +76 -0
  57. package/src/utils/bridgeAsyncGuard.js +37 -0
  58. package/src/utils/cargoTarget.js +15 -0
  59. package/src/utils/cppDts.js +201 -0
  60. package/src/utils/cppFieldBindings.js +23 -0
  61. package/src/utils/dependencyRebuild.js +99 -0
  62. package/src/utils/dirLock.js +71 -0
  63. package/src/utils/downloadAndExtractFile.js +102 -0
  64. package/src/utils/familyManifest.js +17 -0
  65. package/src/utils/findFiles.js +5 -0
  66. package/src/utils/fixPackageName.js +7 -0
  67. package/src/utils/flattenConfigForTable.js +75 -0
  68. package/src/utils/getAbsolutePath.js +14 -0
  69. package/src/utils/getCMakeListsFilePath.js +17 -0
  70. package/src/utils/getOsUserAndGroupId.js +11 -0
  71. package/src/utils/getParentPath.js +12 -0
  72. package/src/utils/hash.js +33 -0
  73. package/src/utils/inputStamp.js +28 -0
  74. package/src/utils/licenseReport.js +94 -0
  75. package/src/utils/linkLayout.js +34 -0
  76. package/src/utils/loadJs.js +31 -0
  77. package/src/utils/loadJson.js +16 -0
  78. package/src/utils/logger.js +110 -0
  79. package/src/utils/ociArtifact.js +124 -0
  80. package/src/utils/overrideDependency.js +99 -0
  81. package/src/utils/provenance.js +54 -0
  82. package/src/utils/pullDockerImage.js +105 -0
  83. package/src/utils/replaceBasePathForDocker.js +18 -0
  84. package/src/utils/resolveEmbindRust.js +66 -0
  85. package/src/utils/runCargo.js +228 -0
  86. package/src/utils/rustBridgeGen.js +1013 -0
  87. package/src/utils/rustMt.js +76 -0
  88. package/src/utils/rustSysroot.js +171 -0
  89. package/src/utils/rustSysrootPin.js +11 -0
  90. package/src/utils/systemKeys.js +29 -0
  91. package/src/utils/targets.js +109 -0
  92. package/src/utils/wasiToolchain.js +47 -0
  93. package/src/utils/writeIfChanged.js +8 -0
  94. package/src/utils/writeJson.js +9 -0
  95. package/vitest.config.js +40 -0
@@ -0,0 +1,215 @@
1
+ import os from 'node:os';
2
+ import fs from 'node:fs';
3
+ import systemKeys from '../utils/systemKeys.js';
4
+ import loadJs from '../utils/loadJs.js';
5
+ import {
6
+ resolveDependencyOverride, resolveDependencyReplace, restampIdentity, resolveExcludedNames, getOverrideKey,
7
+ } from '../utils/overrideDependency.js';
8
+ import loadJson from '../utils/loadJson.js';
9
+ import getParentPath from '../utils/getParentPath.js';
10
+ import getAbsolutePath from '../utils/getAbsolutePath.js';
11
+ import fixPackageName from '../utils/fixPackageName.js';
12
+ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
13
+ import calculateDependencyParameters from './calculateDependencyParameters.js';
14
+ import logger from '../utils/logger.js';
15
+ // import getCmakeParameters from './getCmakeParameters.js';
16
+
17
+ export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
18
+ const config = await loadJs(configDir, configName) || {};
19
+ if (config.excludedDependencies?.length) {
20
+ logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: false }.');
21
+ }
22
+ const overrides = await loadJs(configDir, 'crossbind.overrides');
23
+ const exclude = resolveExcludedNames(overrides);
24
+ const seen = new Set();
25
+ const output = getFilledConfig(config, { isDepend: false, exclude, seen, replaces: overrides });
26
+ output.excludedDependencies = exclude;
27
+
28
+ const build = await loadJs(configDir, 'crossbind.build');
29
+
30
+ if (build) {
31
+ build.withBuildConfig = true;
32
+ }
33
+ output.build = { ...build, ...output.build };
34
+
35
+ if (overrides) {
36
+ Object.entries(overrides).forEach(([key, value]) => {
37
+ if (!value || typeof value !== 'object') {
38
+ logger.info(`crossbind: crossbind.overrides["${key}"] must be an object — { rebuild: true } / { exclude: true } / { replace: ... }.`);
39
+ }
40
+ });
41
+ output.allDependencies.forEach((d) => {
42
+ const resolved = resolveDependencyOverride(overrides, d);
43
+ if (resolved) {
44
+ d.overrideBuild = resolved.override;
45
+ d.rebuild = true;
46
+ }
47
+ });
48
+ const unmatched = Object.keys(overrides).filter((k) => !seen.has(k));
49
+ if (unmatched.length) {
50
+ logger.info(`crossbind: crossbind.overrides entries not matched to any dependency: ${unmatched.join(', ')}. Available: ${[...seen].sort().join(', ')}`);
51
+ }
52
+ }
53
+
54
+ // Consume any dependency already rebuilt from source (buildDependencies writes the marker) so the
55
+ // fresh artifacts win across processes — e.g. Android rebuilds at the Gradle config phase and is
56
+ // consumed when the CMake phase reloads the config.
57
+ output.allDependencies.forEach((d) => {
58
+ const depsDir = `${output.paths.cache}/deps/${d.general.name}`;
59
+ const meta = loadJson(`${depsDir}/.crossbind-rebuild.json`);
60
+ if (!meta || meta.key !== getOverrideKey(d)) return;
61
+ if (!fs.existsSync(`${depsDir}/dist/prebuilt/CMakeLists.txt`)) {
62
+ logger.info(`crossbind: rebuilt cache for "${d.general.name}" is incomplete (missing dist/prebuilt/CMakeLists.txt) — using prebuilt. Rebuild, or reset with \`crossbind clean-deps\`.`);
63
+ return;
64
+ }
65
+ d.paths.output = `${depsDir}/dist`;
66
+ if (d.export.libName.some((n) => fs.existsSync(`${depsDir}/${n}.xcframework`))) {
67
+ d.paths.project = depsDir;
68
+ }
69
+ });
70
+
71
+ output.paths.systemConfig = `${os.homedir()}/.crossbind.json`;
72
+ output.system = loadJson(output.paths.systemConfig) || {};
73
+
74
+ Object.entries(systemKeys).forEach(([key, value]) => {
75
+ if (!(key in output.system)) {
76
+ output.system[key] = value.default;
77
+ }
78
+ });
79
+
80
+ return output;
81
+ }
82
+
83
+ export function getFilledConfig(config, options = { isDepend: false }) {
84
+ const exclude = options.exclude || [];
85
+ const { seen, replaces } = options;
86
+
87
+ const dependencies = (config.dependencies || [])
88
+ .map((d) => {
89
+ const filled = getFilledConfig(d, { isDepend: true, exclude, seen, replaces });
90
+ const replacement = resolveDependencyReplace(replaces, filled);
91
+ if (!replacement) return filled;
92
+ return restampIdentity(getFilledConfig(replacement, { isDepend: true, exclude, seen, replaces }), filled);
93
+ })
94
+ .filter((fd) => {
95
+ const names = [fd.general.name, fd.package?.name, fd.general.alias?.package].filter(Boolean);
96
+ names.forEach((n) => seen?.add(n));
97
+ return !names.some((n) => exclude.includes(n));
98
+ });
99
+
100
+ const newConfig = {
101
+ general: config.general || {},
102
+ dependencies,
103
+ cargoDependencies: config.cargoDependencies || {},
104
+ // 'sync' types the direct surface; 'promise' wraps every generated method return
105
+ // for useWorker-style runtimes (constructors stay sync-typed: await new X()).
106
+ dts: config.dts || 'sync',
107
+ // Opt-in package type publishing: emit <output>/types/index.d.ts over the public
108
+ // headers and wire package.json types/typesVersions (see actions/buildTypes.js).
109
+ types: config.types || false,
110
+ // Cloned: raw crossbind.config modules are import singletons; later path mutations
111
+ // (e.g. rebuild-marker consumption) must not leak into them across loadConfig calls.
112
+ paths: { ...(config.paths || {}) },
113
+ ext: config.ext || {},
114
+ export: config.export || {},
115
+ targetSpecs: config.targetSpecs || [],
116
+ build: config.build || {},
117
+ target: config.target || {},
118
+ extensions: config.extensions || [],
119
+ package: null,
120
+ functions: config.functions || {},
121
+ };
122
+
123
+ if (!['sync', 'promise'].includes(newConfig.dts)) {
124
+ throw new Error(`crossbind: "dts" must be 'sync' or 'promise' (got '${newConfig.dts}').`);
125
+ }
126
+ if (typeof newConfig.types !== 'boolean') {
127
+ throw new Error(`crossbind: "types" must be true or false (got '${newConfig.types}').`);
128
+ }
129
+
130
+ if (newConfig.paths.config && !newConfig.paths.project) {
131
+ newConfig.paths.project = getParentPath(newConfig.paths.config);
132
+ }
133
+
134
+ if (!newConfig.paths.project) {
135
+ newConfig.paths.project = process.cwd();
136
+ } else {
137
+ newConfig.paths.project = getAbsolutePath(null, newConfig.paths.project);
138
+ }
139
+
140
+ newConfig.package = loadJson(`${newConfig.paths.project}/package.json`);
141
+
142
+ if (!newConfig?.general?.name) {
143
+ if (!newConfig.package) {
144
+ throw new Error(`crossbind: no package.json found in ${newConfig.paths.project}. Run crossbind from your project root, or scaffold a project with \`npm create crossbind\`.`);
145
+ }
146
+ newConfig.general.name = fixPackageName(newConfig.package.name) || 'crossbindsample';
147
+ }
148
+
149
+ const getPath = getAbsolutePath.bind(null, newConfig.paths.project);
150
+
151
+ newConfig.paths.base = getPath(newConfig.paths.base) || newConfig.paths.project;
152
+ newConfig.paths.cache = getPath(newConfig.paths.cache) || getPath('.crossbind');
153
+ newConfig.paths.build = getPath(newConfig.paths.build) || getPath(`${newConfig.paths.cache}/build`);
154
+ newConfig.paths.native = (newConfig.paths.native || ['src/native']).map((p) => getPath(p));
155
+ newConfig.paths.module = (newConfig.paths.module || newConfig.paths.native).map((p) => getPath(p));
156
+ newConfig.paths.header = (newConfig.paths.header || newConfig.paths.native).map((p) => getPath(p));
157
+ newConfig.paths.bridge = (newConfig.paths.bridge || [...newConfig.paths.native, newConfig.paths.build])
158
+ .map((p) => getPath(p));
159
+ newConfig.paths.output = getPath(newConfig.paths.output) || newConfig.paths.build;
160
+ newConfig.paths.cmake = options.isDepend ? getPath(getCMakeListsFilePath(newConfig.paths.output)) : (
161
+ getPath(newConfig.paths.cmake || getCMakeListsFilePath(newConfig.paths.project))
162
+ );
163
+ newConfig.paths.cmakeDir = getParentPath(newConfig.paths.cmake);
164
+ newConfig.paths.cli = getParentPath(getParentPath(import.meta.url));
165
+ newConfig.paths.cliCMakeListsTxt = `${newConfig.paths.cli}/assets/cmake/CMakeLists.txt`;
166
+
167
+ newConfig.ext.header = newConfig.ext.header || ['h', 'hpp', 'hxx', 'hh'];
168
+ newConfig.ext.source = newConfig.ext.source || ['c', 'cpp', 'cxx', 'cc'];
169
+ newConfig.ext.module = newConfig.ext.module || ['i'];
170
+
171
+ newConfig.export.type = newConfig.export.type || 'cmake';
172
+ newConfig.export.header = newConfig.export.header || 'include';
173
+ newConfig.export.libPath = getPath(newConfig.export.libPath || 'lib');
174
+ newConfig.export.libName = newConfig.export.libName || [newConfig.general.name];
175
+ newConfig.export.binHeaders = newConfig.export.binHeaders || [];
176
+ newConfig.export.bundle = newConfig.export.bundle ?? true;
177
+
178
+ newConfig.allDependencies = (() => {
179
+ const output = {};
180
+ [...newConfig.dependencies, ...newConfig.dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
181
+ output[d.paths.project] = d;
182
+ });
183
+ return Object.values(output);
184
+ })();
185
+
186
+ newConfig.extensions?.forEach(e => {
187
+ e?.loadConfig?.after(newConfig);
188
+ });
189
+
190
+ if (newConfig.target.runtime !== 'mt' && newConfig.allDependencies.some((d) => d?.target?.runtime === 'mt')) {
191
+ newConfig.target.runtime = 'mt';
192
+ }
193
+
194
+ newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => {
195
+ // Source-cmake packages ship no prebuilt at all: their own CMakeLists (at the
196
+ // package root, not a generated dist/prebuilt one) is compiled into the consuming
197
+ // build via add_subdirectory, so they serve every target.
198
+ const isSourceCmakePackage = newConfig.export?.type === 'cmake'
199
+ && newConfig.paths.cmake !== newConfig.paths.cliCMakeListsTxt
200
+ && !newConfig.paths.cmakeDir.endsWith('/prebuilt')
201
+ && fs.existsSync(newConfig.paths.cmake);
202
+
203
+ return (
204
+ isSourceCmakePackage
205
+ || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
206
+ || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
207
+ || (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
208
+ );
209
+ });
210
+
211
+ newConfig.dependencyParameters = calculateDependencyParameters(newConfig);
212
+ // newConfig.cmakeParameters = getCmakeParameters(newConfig);
213
+
214
+ return newConfig;
215
+ }
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { cargoTripleFor } from './cargoTarget.js';
4
+ import { isMtWasm, assertMtRustToolchain, cargoTargetDirFor, cargoBuildInvocation } from './rustMt.js';
5
+ import runCargo from './runCargo.js';
6
+ import { sysrootFor } from './rustSysroot.js';
7
+ import { relPath } from './rustBridgeGen.js';
8
+ import writeIfChanged from './writeIfChanged.js';
9
+
10
+ const SUPER = '_app_super';
11
+
12
+ // App-local Rust surfaces (imported .rs files) live as rlib bridge crates under
13
+ // <project>/.crossbind/rust-bridges/ (written by the bundler transformer on import). They are
14
+ // bundled into ONE super staticlib here - a single libstd, so the consumer may safely
15
+ // force_load/whole-archive exactly this archive next to keep-symbol-linked Rust packages.
16
+ export default function buildAppRustCrates(target, cacheDir, cargoDependencies = null, log = console.log) {
17
+ const root = `${cacheDir}/rust-bridges`;
18
+ if (!fs.existsSync(root)) return [];
19
+ let crates = fs.readdirSync(root, { withFileTypes: true })
20
+ .filter((e) => e.isDirectory() && e.name !== SUPER && fs.existsSync(`${root}/${e.name}/Cargo.toml`))
21
+ .map((e) => e.name);
22
+
23
+ // Prune bridges whose source of truth is gone. An app-local surface whose .rs was deleted
24
+ // breaks cargo on the missing #[path] target; a crate_ bridge whose cargoDependencies entry
25
+ // was removed silently keeps dead registrations in every later binary. crate_ pruning only
26
+ // runs when the caller passes the current cargoDependencies - null means "don't judge".
27
+ crates = crates.filter((name) => {
28
+ const dir = `${root}/${name}`;
29
+ const libRs = `${dir}/src/lib.rs`;
30
+ const src = fs.existsSync(libRs) ? fs.readFileSync(libRs, 'utf8') : '';
31
+ const pathMod = src.match(/#\[path = "(.+?)"\]\s*\r?\nmod user;/);
32
+ let stale = false;
33
+ if (pathMod) {
34
+ // #[path] is written relative to the file that carries it, so it has to be resolved
35
+ // against that directory - resolving it against cwd finds nothing and prunes a
36
+ // perfectly live bridge.
37
+ stale = !fs.existsSync(path.resolve(path.dirname(libRs), pathMod[1]));
38
+ } else if (cargoDependencies && name.startsWith('crate_')) {
39
+ stale = !Object.keys(cargoDependencies)
40
+ .some((c) => `crate_${c.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}` === name);
41
+ }
42
+ if (stale) {
43
+ fs.rmSync(dir, { recursive: true, force: true });
44
+ log(`crossbind: pruned stale rust bridge '${name}' - its source is gone`);
45
+ }
46
+ return !stale;
47
+ });
48
+ if (crates.length === 0) return [];
49
+
50
+ const isMt = isMtWasm(target);
51
+ // true in the image, a downloaded artifact path on a host that has one, false where mt must
52
+ // still rebuild std - and only that last case needs a nightly toolchain here.
53
+ const sysroot = sysrootFor(target);
54
+ if (isMt && !sysroot) assertMtRustToolchain();
55
+
56
+ const superDir = `${root}/${SUPER}`;
57
+ const manifest = [
58
+ '# Generated by crossbind - do not edit. Bundles every app-local Rust surface.',
59
+ '[package]',
60
+ 'name = "crossbind-app-super"',
61
+ 'version = "0.0.0"',
62
+ 'edition = "2021"',
63
+ '',
64
+ '[lib]',
65
+ 'name = "crossbind_app_super"',
66
+ 'crate-type = ["staticlib"]',
67
+ '',
68
+ '[dependencies]',
69
+ ...crates.map((c) => `${c.replaceAll('_', '-')}-crossbind-app = { path = "${relPath(superDir, `${root}/${c}`)}" }`),
70
+ '',
71
+ '[profile.release]',
72
+ 'panic = "abort"',
73
+ '',
74
+ '[workspace]',
75
+ '',
76
+ ].join('\n');
77
+ const lib = [
78
+ '// Generated by crossbind - do not edit. The underscore uses bundle each bridge rlib (and',
79
+ '// its init-array registrations) into this single staticlib.',
80
+ ...crates.map((c) => `use ${c}_crossbind_app as _;`),
81
+ '',
82
+ ].join('\n');
83
+ writeIfChanged(`${superDir}/Cargo.toml`, manifest);
84
+ writeIfChanged(`${superDir}/src/lib.rs`, lib);
85
+
86
+ const triple = cargoTripleFor(target);
87
+ if (!triple) throw new Error(`crossbind: app rust crates do not support platform '${target.platform}/${target.arch}'.`);
88
+
89
+ const targetDir = cargoTargetDirFor(superDir, target);
90
+ const { args, rustflags, panic, allowUnstable } = cargoBuildInvocation({
91
+ target,
92
+ triple,
93
+ targetDir,
94
+ manifestPath: `${superDir}/Cargo.toml`,
95
+ sysroot,
96
+ });
97
+ const build = runCargo(args, {
98
+ rustflags, panic, allowUnstable, target,
99
+ });
100
+ if (build.status !== 0) {
101
+ throw new Error(`crossbind: cargo build failed for the app rust super-crate (${triple}${isMt && !sysroot ? ', mt/build-std' : ''}${sysroot ? `, ${isMt ? 'mt' : 'st'} sysroot` : ''})`);
102
+ }
103
+ const libFile = `${targetDir}/${triple}/release/libcrossbind_app_super.a`;
104
+ if (!fs.existsSync(libFile)) throw new Error(`crossbind: expected cargo output ${libFile} not found`);
105
+ return [libFile];
106
+ }
@@ -0,0 +1,76 @@
1
+ // Pure helpers for -bin packages (contract C): the recipe's `bin` block is the single
2
+ // source for the tool map; everything else (json, npmignore, dispatcher) derives from it.
3
+
4
+ export function validateBinConfig(bin) {
5
+ const tools = bin?.tools ?? {};
6
+ const names = Object.keys(tools);
7
+ if (names.length === 0) throw new Error('crossbind: bin.tools is empty');
8
+ const binaries = names.filter((name) => tools[name].kind === 'binary');
9
+ const multicall = names.filter((name) => tools[name].kind === 'multicall-entry');
10
+ if (multicall.length > 0 && binaries.length !== 1) {
11
+ throw new Error('crossbind: multicall-entry tools need exactly one kind:"binary" host tool');
12
+ }
13
+ return { tools, names, binaries, multicall };
14
+ }
15
+
16
+ export function renderBinJson(bin) {
17
+ return `${JSON.stringify({ tools: bin.tools }, null, 2)}\n`;
18
+ }
19
+
20
+ export function renderBinNpmignore(bin) {
21
+ const published = Object.entries(bin.tools ?? {})
22
+ .filter(([, tool]) => tool.publish)
23
+ .map(([name]) => name);
24
+ return `${[
25
+ '# GENERATED by crossbind from the recipe bin map (crossbind.build.js) - do not edit.',
26
+ '*.xcframework.zip',
27
+ '*.tgz',
28
+ '.crossbind',
29
+ '*.dylib',
30
+ '# bin package: ship only the tools the bin map declares publish:true, plus share/ data.',
31
+ 'dist/prebuilt/**/bin/*',
32
+ ...published.map((name) => `!dist/prebuilt/**/bin/${name}`),
33
+ 'dist/prebuilt/**/lib',
34
+ 'dist/prebuilt/**/include',
35
+ 'dist/prebuilt/**/share/man',
36
+ 'dist/prebuilt/**/share/doc',
37
+ 'dist/prebuilt/**/share/bash-completion',
38
+ ].join('\n')}\n`;
39
+ }
40
+
41
+ // npm command surface: one `<tool>-wasi` shim per shipped tool, all sharing the copied runner.
42
+ export function renderBinCommands(bin) {
43
+ if (bin.commands === false) return {};
44
+ return Object.fromEntries(
45
+ Object.entries(bin.tools ?? {})
46
+ .filter(([, tool]) => tool.publish)
47
+ .map(([name]) => [`${name}-wasi`, `bin/${name}-wasi.mjs`]),
48
+ );
49
+ }
50
+
51
+ export function renderCommandStub(tool) {
52
+ return [
53
+ '#!/usr/bin/env node',
54
+ '// GENERATED by crossbind from the recipe bin map (crossbind.build.js) - do not edit.',
55
+ "import { run } from 'crossbind/src/runtime/wasiRun.mjs';",
56
+ '',
57
+ `await run(import.meta.url, '${tool}');`,
58
+ '',
59
+ ].join('\n');
60
+ }
61
+
62
+ export function renderDispatcher(bin) {
63
+ const { binaries, multicall } = validateBinConfig(bin);
64
+ const host = binaries[0];
65
+ const symbolOf = (tool) => `${tool}_multicall_main`;
66
+ return [
67
+ '#include <cstring>',
68
+ ...[host, ...multicall].map((tool) => `int ${symbolOf(tool)}(int, char**);`),
69
+ 'int main(int argc, char** argv) {',
70
+ ' const char* tool = argc > 1 ? argv[1] : "";',
71
+ ...multicall.map((tool) => ` if (!std::strcmp(tool, "${tool}")) return ${symbolOf(tool)}(argc - 1, argv + 1);`),
72
+ ` return ${symbolOf(host)}(argc, argv);`,
73
+ '}',
74
+ '',
75
+ ].join('\n');
76
+ }
@@ -0,0 +1,37 @@
1
+ // SWIG registers _JSPI-suffixed methods with emscripten::async(). Registering
2
+ // such a binding in a build WITHOUT -sJSPI aborts emsdk 6 DEBUG builds at
3
+ // embind registration time ("Async bindings are only supported with JSPI"),
4
+ // killing the whole module before anything runs. One bridge .cpp is shared by
5
+ // every target of a package, so the guard has to be a compile-time condition:
6
+ // wrap each async registration in #ifdef CROSSBIND_JSPI, and let createLib define
7
+ // CROSSBIND_JSPI only for targets whose emccFlags carry -sJSPI.
8
+ export default function guardAsyncBindings(bridgeText) {
9
+ if (!bridgeText.includes('emscripten::async()')) {
10
+ return bridgeText;
11
+ }
12
+
13
+ const lines = bridgeText.split('\n');
14
+ const out = [];
15
+ for (let i = 0; i < lines.length; i += 1) {
16
+ const line = lines[i];
17
+ const alreadyGuarded = i > 0 && lines[i - 1].trim() === '#ifdef CROSSBIND_JSPI';
18
+ if (!line.includes('emscripten::async()') || alreadyGuarded) {
19
+ out.push(line);
20
+ continue;
21
+ }
22
+ // Registrations are one chained call per line, optionally closing the
23
+ // statement. Keep any trailing `;` OUTSIDE the guard so the chain (or
24
+ // an empty statement) stays valid when CROSSBIND_JSPI is undefined.
25
+ const match = /^(\s*)(.*?);?\s*$/.exec(line);
26
+ const indent = match[1];
27
+ const call = match[2];
28
+ const endsStatement = /;\s*$/.test(line);
29
+ out.push('#ifdef CROSSBIND_JSPI');
30
+ out.push(indent + call);
31
+ out.push('#endif');
32
+ if (endsStatement) {
33
+ out.push(`${indent};`);
34
+ }
35
+ }
36
+ return out.join('\n');
37
+ }
@@ -0,0 +1,15 @@
1
+ // Maps a crossbind build target to the cargo target triple. Rust is built on the host (the rust
2
+ // toolchain is host-installed), so the host must have the matching linker/SDK: Xcode for apple,
3
+ // a sourced emsdk for wasm, an NDK for android.
4
+ export function cargoTripleFor(target) {
5
+ switch (target.platform) {
6
+ case 'wasm':
7
+ return 'wasm32-unknown-emscripten';
8
+ case 'ios':
9
+ return target.arch === 'iphonesimulator' ? 'aarch64-apple-ios-sim' : 'aarch64-apple-ios';
10
+ case 'android':
11
+ return target.arch === 'x86_64' ? 'x86_64-linux-android' : 'aarch64-linux-android';
12
+ default:
13
+ return null; // wasi (no rust wasip3 target yet) and anything else: unsupported
14
+ }
15
+ }
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import writeIfChanged from './writeIfChanged.js';
4
+
5
+ // Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
6
+ // binding-rules surface (classes, public methods, primitives/string/shared_ptr), skip
7
+ // anything else WITH a log line, and fall back to `any` for exported symbols the parser
8
+ // did not understand - the export list itself always comes from the bridge (exports.json).
9
+
10
+ const NUMBER_TYPES = new Set([
11
+ 'int', 'long', 'short', 'float', 'double', 'size_t', 'unsigned',
12
+ 'unsigned char', 'signed char',
13
+ 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
14
+ 'long long', 'unsigned int', 'unsigned long', 'unsigned short', 'unsigned long long',
15
+ ]);
16
+
17
+ function tsType(raw, classNames, { nullableSharedPtr = false } = {}) {
18
+ let t = raw.trim().replace(/\bconst\b/g, '').replace(/&/g, '').trim().replace(/\s+/g, ' ');
19
+ if (t.includes('*')) return null;
20
+ const vector = t.match(/^std::vector<([\s\S]+)>$/);
21
+ if (vector) {
22
+ const inner = tsType(vector[1], classNames);
23
+ return inner === null || inner === 'void' ? null : `CppVector<${inner}>`;
24
+ }
25
+ const shared = t.match(/^std::shared_ptr<\s*(\w+)\s*>$/);
26
+ if (shared) {
27
+ if (!classNames.has(shared[1])) return null;
28
+ // Embind shared_ptr returns can resolve to null; parameters take the plain object.
29
+ return nullableSharedPtr ? `${shared[1]} | null` : shared[1];
30
+ }
31
+ if (t === 'void') return 'void';
32
+ if (t === 'bool') return 'boolean';
33
+ if (NUMBER_TYPES.has(t)) return 'number';
34
+ if (t === 'std::string') return 'string';
35
+ if (classNames.has(t)) return t;
36
+ return null;
37
+ }
38
+
39
+ function parseArgs(rawArgs, classNames) {
40
+ const trimmed = rawArgs.trim();
41
+ if (trimmed === '' || trimmed === 'void') return [];
42
+ const parts = [];
43
+ let depth = 0;
44
+ let current = '';
45
+ for (const ch of trimmed) {
46
+ if (ch === '<' || ch === '(') depth += 1;
47
+ if (ch === '>' || ch === ')') depth -= 1;
48
+ if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else current += ch;
49
+ }
50
+ parts.push(current);
51
+ return parts.map((part, i) => {
52
+ const noDefault = part.split('=')[0].trim();
53
+ // Declaration-style unnamed parameter: the whole token is a type.
54
+ const unnamed = tsType(noDefault, classNames);
55
+ if (unnamed !== null && unnamed !== 'void') return { name: `arg${i}`, type: unnamed };
56
+ const m = noDefault.match(/^(.*?)([A-Za-z_]\w*)$/s);
57
+ if (!m) return null;
58
+ const type = tsType(m[1], classNames);
59
+ if (type === null || type === 'void') return null;
60
+ return { name: m[2] || `arg${i}`, type };
61
+ });
62
+ }
63
+
64
+ // Statements are collected at brace depth 0 of the class body; inline bodies and
65
+ // member-initialiser lists after the argument list are skipped.
66
+ function bodyStatements(body) {
67
+ const statements = [];
68
+ let current = '';
69
+ let depth = 0;
70
+ for (let i = 0; i < body.length; i += 1) {
71
+ const ch = body[i];
72
+ if (ch === '{') { depth += 1; continue; }
73
+ if (ch === '}') { depth -= 1; if (depth === 0) { statements.push(current); current = ''; } continue; }
74
+ if (depth > 0) continue;
75
+ if (ch === ';') { statements.push(current); current = ''; continue; }
76
+ current += ch;
77
+ }
78
+ statements.push(current);
79
+ return statements.map((s) => s.trim()).filter(Boolean);
80
+ }
81
+
82
+ export function parseCppSurface(source, log = console.log) {
83
+ const clean = source
84
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
85
+ .replace(/\/\/[^\n]*/g, ' ')
86
+ .replace(/^[ \t]*#[^\n]*$/gm, ' ');
87
+
88
+ const classes = [];
89
+ const classNames = new Set();
90
+ const classRe = /\b(class|struct)\s+([A-Za-z_]\w*)\s*(?::[^{]*)?\{/g;
91
+ const found = [];
92
+ let m = classRe.exec(clean);
93
+ while (m) {
94
+ let depth = 1;
95
+ let i = classRe.lastIndex;
96
+ while (i < clean.length && depth > 0) {
97
+ if (clean[i] === '{') depth += 1;
98
+ if (clean[i] === '}') depth -= 1;
99
+ i += 1;
100
+ }
101
+ found.push({ kind: m[1], name: m[2], body: clean.slice(classRe.lastIndex, i - 1) });
102
+ classNames.add(m[2]);
103
+ m = classRe.exec(clean);
104
+ }
105
+
106
+ // Value-semantics fields only: these are the types embind's .property can expose
107
+ // without ownership questions (and the worker clone can carry).
108
+ const FIELD_TYPES = new Set(['number', 'boolean', 'string']);
109
+
110
+ for (const cls of found) {
111
+ let access = cls.kind === 'struct' ? 'public' : 'private';
112
+ let ctor = null;
113
+ const methods = [];
114
+ const fields = [];
115
+ for (let statement of bodyStatements(cls.body)) {
116
+ const sections = statement.match(/\b(public|private|protected)\s*:\s*([\s\S]*)$/);
117
+ if (sections) { access = sections[1]; statement = sections[2].trim(); }
118
+ if (!statement || access !== 'public') continue;
119
+ statement = statement.replace(/\)\s*:\s*[\s\S]*$/, ')').replace(/\)\s*const$/, ')').trim();
120
+
121
+ const sig = statement.match(/^(static\s+)?(?:explicit\s+)?([\w:<>,\s*&]*?)\s*\b([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/);
122
+ if (!sig) {
123
+ const decl = statement.split('=')[0].trim();
124
+ const field = !statement.includes('(') && !/^(static|using|typedef|friend|enum)\b/.test(decl)
125
+ ? decl.match(/^(.*?)\b([A-Za-z_]\w*)$/s)
126
+ : null;
127
+ if (field) {
128
+ const type = tsType(field[1], classNames);
129
+ if (type !== null && FIELD_TYPES.has(type)) fields.push({ name: field[2], type });
130
+ }
131
+ continue;
132
+ }
133
+ const [, staticKw, retRaw, name, argsRaw] = sig;
134
+ const args = parseArgs(argsRaw, classNames);
135
+ if (args.includes(null)) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported parameter type)`); continue; }
136
+ if (name === cls.name && retRaw.trim() === '') { ctor = { args }; continue; }
137
+ if (name.startsWith('~')) continue;
138
+ const ret = tsType(retRaw, classNames, { nullableSharedPtr: true });
139
+ if (ret === null) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported return type '${retRaw.trim()}')`); continue; }
140
+ methods.push({ name, isStatic: Boolean(staticKw), args, ret });
141
+ }
142
+ classes.push({ name: cls.name, ctor, methods, fields });
143
+ }
144
+ return { classes };
145
+ }
146
+
147
+ export function emitCppDts(model, exportNames, mode = 'sync') {
148
+ const wrap = (t) => (mode === 'promise' ? `Promise<${t}>` : t);
149
+ const out = ['// Generated by crossbind - do not edit. Values are usable after init().', ''];
150
+ const usesVector = model.classes.some((cls) => (cls.ctor?.args ?? []).concat(cls.methods.flatMap((m) => [...m.args, { type: m.ret }]))
151
+ .some((a) => a.type?.includes('CppVector<')));
152
+ if (usesVector) {
153
+ out.push('/** An embind std::vector proxy. Convert with Module.toArray / Module.toVector. */');
154
+ out.push('export interface CppVector<T> {');
155
+ out.push(` size(): ${wrap('number')};`);
156
+ out.push(` get(index: number): ${wrap('T')};`);
157
+ out.push(` push_back(value: T): ${wrap('void')};`);
158
+ out.push(` delete(): ${wrap('void')};`);
159
+ out.push('}');
160
+ }
161
+ const emitted = new Set();
162
+ for (const cls of model.classes) {
163
+ if (!exportNames.includes(cls.name)) continue;
164
+ emitted.add(cls.name);
165
+ out.push(`export declare class ${cls.name} {`);
166
+ if (cls.ctor) out.push(` constructor(${cls.ctor.args.map((a) => `${a.name}: ${a.type}`).join(', ')});`);
167
+ else out.push(' private constructor();');
168
+ for (const field of cls.fields ?? []) {
169
+ out.push(` ${field.name}: ${field.type};`);
170
+ }
171
+ for (const method of cls.methods) {
172
+ out.push(` ${method.isStatic ? 'static ' : ''}${method.name}(${method.args.map((a) => `${a.name}: ${a.type}`).join(', ')}): ${wrap(method.ret)};`);
173
+ }
174
+ out.push('}');
175
+ }
176
+ for (const name of exportNames) {
177
+ if (!emitted.has(name)) out.push(`export declare const ${name}: any;`);
178
+ }
179
+ out.push('export declare let AllSymbols: Record<string, unknown>;');
180
+ out.push('export declare function initNative(config?: Record<string, unknown>): Promise<unknown>;');
181
+ out.push('');
182
+ return out.join('\n');
183
+ }
184
+
185
+ // Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
186
+ // user's header. Package headers are skipped: their types ship with the package itself.
187
+ export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
188
+ if (!fs.existsSync(exportsFile)) return;
189
+ const relative = path.relative(projectPath, headerFile);
190
+ if (relative.startsWith('..')) return;
191
+ let exportNames;
192
+ try {
193
+ exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
194
+ } catch (e) {
195
+ log(`crossbind: dts: unreadable exports file ${exportsFile} (${e.message})`);
196
+ return;
197
+ }
198
+ if (!Array.isArray(exportNames)) return;
199
+ const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
200
+ writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, emitCppDts(model, exportNames, dtsMode));
201
+ }