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.
Files changed (95) hide show
  1. package/AGENTS.md +92 -0
  2. package/CHANGELOG.md +156 -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 +307 -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 +46 -0
@@ -0,0 +1,228 @@
1
+ import { spawnSync, execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import state from '../state/index.js';
6
+ import getOsUserAndGroupId from './getOsUserAndGroupId.js';
7
+ import replaceBasePathForDockerUtil, { DOCKER_BASE } from './replaceBasePathForDocker.js';
8
+ import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
9
+
10
+ // Every cargo invocation crossbind makes goes through here.
11
+ //
12
+ // Cargo takes flags from three channels: the process environment, $CARGO_HOME/config.toml, and any
13
+ // .cargo/config.toml found by walking up from the CURRENT WORKING DIRECTORY. The last one is the
14
+ // dangerous leg - a file above the crate can hand rustc a different compiler, extra codegen flags
15
+ // or -Z unstable features, and cargo discovers it purely from cwd. So: rustflags are injected
16
+ // through the process environment (which wins over both config channels), and the config legs are
17
+ // closed by running from a neutral directory with a crossbind-owned CARGO_HOME.
18
+ //
19
+ // Where it runs follows the runner. A containerized build runs cargo in the image that carries the
20
+ // pinned toolchain, so a project needs Docker and Node and no host Rust. The registry cache is
21
+ // bind-mounted rather than kept in a named volume, which keeps it shared across projects, visible
22
+ // from the host, and addressable - bridge generation reads crate sources out of it.
23
+
24
+ // Cargo splits this variable on 0x1F, one argv per flag - never a space.
25
+ const RUSTFLAGS_SEPARATOR = String.fromCharCode(0x1f);
26
+
27
+ // The environment is rebuilt from nothing and only these survive, so a variable that can redirect
28
+ // the compiler - RUSTC_WRAPPER, CARGO_BUILD_RUSTC, CARGO_TARGET_<TRIPLE>_LINKER, RUSTFLAGS,
29
+ // RUSTUP_TOOLCHAIN, RUSTC_BOOTSTRAP - cannot reach cargo from the caller's environment.
30
+ const ALLOWED_ENV = [
31
+ /^PATH$/, /^HOME$/, /^USER$/, /^LOGNAME$/, /^SHELL$/, /^TERM$/,
32
+ /^TMPDIR$/, /^TEMP$/, /^TMP$/,
33
+ /^LANG$/, /^LC_[A-Z_]+$/,
34
+ /^RUSTUP_HOME$/,
35
+ /^SSH_AUTH_SOCK$/, // git dependencies over ssh
36
+ /^CARGO_NET_/, /^CARGO_REGISTRIES_/, // offline/vendored builds and private registries
37
+ /^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
38
+ /^SYSTEMROOT$/i, /^WINDIR$/i, /^USERPROFILE$/i, /^PROGRAMDATA$/i, // windows cannot spawn without these
39
+ ];
40
+
41
+ // Recent cargo applies config.toml only, but the extensionless name is still read by older
42
+ // versions - both are checked so a behaviour change between versions cannot open the gap.
43
+ const CONFIG_NAMES = ['config.toml', 'config'];
44
+
45
+ // Where the image puts CARGO_HOME (base.Dockerfile), and a directory outside any project so
46
+ // cargo's upward config search finds nothing.
47
+ const CONTAINER_CARGO_HOME = '/var/cache/crossbind/cargo';
48
+ const CONTAINER_CWD = '/tmp/crossbind-cargo';
49
+
50
+ // Crossbind-owned and machine-wide: $CARGO_HOME/config.toml is one of the discovery legs, so the
51
+ // registry cache lives somewhere we can assert is clean instead of in the user's ~/.cargo. It is
52
+ // also the directory bind-mounted into the container, which is why it must not sit inside a
53
+ // project - `pnpm run clear` would otherwise throw away every downloaded crate.
54
+ export function cargoHome() {
55
+ return path.join(os.homedir(), '.crossbind', 'cargo');
56
+ }
57
+
58
+ // The inverse of the two bind mounts. cargo reports paths as the container sees them, and the
59
+ // mounts are the only way in, so mapping their prefixes back is exact rather than a guess. Bridge
60
+ // generation reads crate sources straight off the host: without this it looks for a path that
61
+ // exists only inside the container, finds no source, and silently emits a bridge binding nothing.
62
+ export function toHostPath(containerPath) {
63
+ const base = state.config?.paths?.base;
64
+ const mounts = [[CONTAINER_CARGO_HOME, cargoHome()], ...(base ? [[DOCKER_BASE, base]] : [])];
65
+ const hit = mounts.find(([from]) => containerPath === from || containerPath.startsWith(`${from}/`));
66
+ return hit ? path.join(hit[1], containerPath.slice(hit[0].length)) : containerPath;
67
+ }
68
+
69
+ // Outside the project and outside $HOME, so the upward walk never reaches ~/.cargo or a config
70
+ // the project tree happens to carry.
71
+ export function neutralCwd() {
72
+ return path.join(os.tmpdir(), 'crossbind-cargo');
73
+ }
74
+
75
+ export function assertCleanConfigChain(home, cwd) {
76
+ for (const name of CONFIG_NAMES) {
77
+ const file = path.join(home, name);
78
+ if (fs.existsSync(file)) {
79
+ throw new Error(`crossbind: unexpected cargo config at ${file} - crossbind owns this CARGO_HOME and never writes one. Remove it and build again.`);
80
+ }
81
+ }
82
+ for (let dir = cwd; ; dir = path.dirname(dir)) {
83
+ for (const name of CONFIG_NAMES) {
84
+ const file = path.join(dir, '.cargo', name);
85
+ if (fs.existsSync(file)) {
86
+ throw new Error(`crossbind: ${file} would apply to this build - cargo discovers config by walking up from the working directory. crossbind builds from a neutral directory to avoid that; move the file or report this.`);
87
+ }
88
+ }
89
+ if (path.dirname(dir) === dir) return;
90
+ }
91
+ }
92
+
93
+ // iOS links with Xcode, which is in no image, so its Rust stays on the host - as does every build
94
+ // under RUNNER=LOCAL.
95
+ export function cargoRunner(target) {
96
+ if (target?.platform === 'ios') return 'LOCAL';
97
+ const runner = state.config?.system?.RUNNER;
98
+ return runner === 'DOCKER_RUN' || runner === 'DOCKER_EXEC' ? runner : 'LOCAL';
99
+ }
100
+
101
+ function allowedEnv() {
102
+ const env = {};
103
+ Object.entries(process.env).forEach(([key, value]) => {
104
+ if (ALLOWED_ENV.some((allowed) => allowed.test(key))) env[key] = value;
105
+ });
106
+ return env;
107
+ }
108
+
109
+ function cargoEnv({
110
+ home, rustflags, panic, allowUnstable,
111
+ }) {
112
+ const env = { CARGO_HOME: home };
113
+ // Depth defence: -Z fails with E0554 instead of quietly enabling unstable behaviour. The value
114
+ // -1 disables unstable features on EVERY channel, nightly included, so the one caller that
115
+ // still needs -Zbuild-std has to opt out - and only until that path is deleted. Leaving it
116
+ // unset is still safe: the caller's own RUSTC_BOOTSTRAP never survives the allowlist.
117
+ if (!allowUnstable) env.RUSTC_BOOTSTRAP = '-1';
118
+ if (rustflags.length) env.CARGO_ENCODED_RUSTFLAGS = rustflags.join(RUSTFLAGS_SEPARATOR);
119
+ // The environment beats the crate's own [profile.release], so a hand-written crate cannot opt
120
+ // out of the panic strategy the std it links against was built with.
121
+ if (panic) env.CARGO_PROFILE_RELEASE_PANIC = panic;
122
+ return env;
123
+ }
124
+
125
+ // docker exec attaches to a container the user created; crossbind never creates one, and until
126
+ // now nothing checked what it carries. A container missing the cargo mount is the worst case: cargo
127
+ // keeps its registry inside the container, so bridge generation reads a crate source from a host
128
+ // path that was never written and emits a bridge that binds nothing - silently. Fail first, and say
129
+ // exactly how to fix it, because the container name and mounts are otherwise undocumented.
130
+ function createHint(name, base, home, image) {
131
+ return [
132
+ 'Create it with:',
133
+ ` docker run -d --name ${name} \\`,
134
+ ` -v ${base}:${DOCKER_BASE} \\`,
135
+ ` -v ${home}:${CONTAINER_CARGO_HOME} \\`,
136
+ ` ${image} sleep infinity`,
137
+ ].join('\n');
138
+ }
139
+
140
+ function samePath(a, b) {
141
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
142
+ return real(a) === real(b);
143
+ }
144
+
145
+ function assertExecContainer(name, base, home, image) {
146
+ const hint = createHint(name, base, home, image);
147
+ let info;
148
+ try {
149
+ // stderr is dropped: docker's own "No such container" would land before the message below.
150
+ info = JSON.parse(execFileSync('docker', ['container', 'inspect', name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }))[0];
151
+ } catch {
152
+ throw new Error(`crossbind: RUNNER=DOCKER_EXEC needs a container named '${name}', which does not exist.\n${hint}`);
153
+ }
154
+ if (!info?.State?.Running) {
155
+ throw new Error(`crossbind: the container '${name}' exists but is not running. Start it with \`docker start ${name}\`, or recreate it.\n${hint}`);
156
+ }
157
+ for (const [destination, source] of [[DOCKER_BASE, base], [CONTAINER_CARGO_HOME, home]]) {
158
+ const mount = (info.Mounts ?? []).find((m) => m.Destination === destination);
159
+ if (!mount || !samePath(mount.Source, source)) {
160
+ const found = mount ? ` - it mounts ${mount.Source} there` : '';
161
+ throw new Error(`crossbind: the container '${name}' does not mount ${source} at ${destination}${found}. It predates the cargo mount, so recreate it.\n${hint}`);
162
+ }
163
+ }
164
+ // `docker run --workdir` creates the directory; `docker exec --workdir` does not, and cargo
165
+ // dies with an OCI "chdir to cwd" error that names nothing the user can act on.
166
+ try {
167
+ execFileSync('docker', ['exec', '--user', getOsUserAndGroupId(), name, 'mkdir', '-p', CONTAINER_CWD], { stdio: 'ignore' });
168
+ } catch (e) {
169
+ throw new Error(`crossbind: could not create the working directory ${CONTAINER_CWD} inside '${name}'.\n${hint}`, { cause: e });
170
+ }
171
+ }
172
+
173
+ export default function runCargo(args, {
174
+ cwd, rustflags = [], panic, capture = false, maxBuffer, allowUnstable = false, target,
175
+ } = {}) {
176
+ const home = cargoHome();
177
+ const workdir = cwd ?? neutralCwd();
178
+ fs.mkdirSync(home, { recursive: true });
179
+ fs.mkdirSync(workdir, { recursive: true });
180
+ // Checked on the host in both modes: the container sees the same directory through the mount.
181
+ assertCleanConfigChain(home, workdir);
182
+
183
+ const runner = cargoRunner(target);
184
+ const stdio = capture ? ['ignore', 'pipe', 'pipe'] : 'inherit';
185
+ const options = { stdio, ...(capture ? { encoding: 'utf8' } : {}), ...(maxBuffer ? { maxBuffer } : {}) };
186
+
187
+ if (runner === 'LOCAL') {
188
+ return spawnSync('cargo', args, {
189
+ cwd: workdir,
190
+ env: { ...allowedEnv(), ...cargoEnv({ home, rustflags, panic, allowUnstable }) },
191
+ ...options,
192
+ });
193
+ }
194
+
195
+ const { base } = state.config.paths;
196
+ const role = imageRoleFor(target);
197
+ // Google ships the linux NDK host tools for x86_64 only.
198
+ const platform = target?.platform === 'android' ? 'linux/amd64' : undefined;
199
+
200
+ // Only what cargo needs travels in: the image already carries PATH, RUSTUP_HOME and the
201
+ // toolchain, and the host's PATH would be meaningless there.
202
+ const env = cargoEnv({
203
+ home: CONTAINER_CARGO_HOME, rustflags, panic, allowUnstable,
204
+ });
205
+ const envArgs = Object.entries(env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
206
+
207
+ let runnerArgs;
208
+ if (runner === 'DOCKER_EXEC') {
209
+ const name = getDockerContainerName(base, role);
210
+ assertExecContainer(name, base, home, getDockerImage(role, platform));
211
+ runnerArgs = ['exec', ...envArgs,
212
+ '--user', getOsUserAndGroupId(),
213
+ '--workdir', CONTAINER_CWD,
214
+ name];
215
+ } else {
216
+ pullDockerImage(role, platform);
217
+ runnerArgs = ['run', '--rm',
218
+ ...(platform ? ['--platform', platform] : []),
219
+ '-v', `${base}:${DOCKER_BASE}`,
220
+ '-v', `${home}:${CONTAINER_CARGO_HOME}`,
221
+ ...envArgs,
222
+ '--user', getOsUserAndGroupId(),
223
+ '--workdir', CONTAINER_CWD,
224
+ getDockerImage(role, platform)];
225
+ }
226
+
227
+ return spawnSync('docker', [...runnerArgs, 'cargo', ...replaceBasePathForDockerUtil(args, base)], options);
228
+ }