crossbind 2.0.0-beta.53 → 2.0.0-beta.56

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 CHANGED
@@ -33,7 +33,7 @@ Side-quests:
33
33
 
34
34
  ## Public API contract
35
35
 
36
- Anything exported from `src/index.js` is consumed by `plugins/*` and a few CLI scripts (`scripts/check-*.js`). Treat its surface as semver-public — additive changes free, breaking changes need a beta bump and a CHANGELOG note.
36
+ Anything exported from `src/index.js` is consumed by `plugins/*` and a few CLI scripts (`scripts/check-*.js`). Treat its surface as semver-public — additive changes free, breaking changes need a beta bump and a human-authored `releases/crossbind/<version>.md` note.
37
37
 
38
38
  Currently exported (snapshot):
39
39
 
package/README.md CHANGED
@@ -87,7 +87,7 @@ adapters.
87
87
  ## Quick start
88
88
 
89
89
  Prefer to scaffold and configure the project yourself? You need
90
- [Node.js 22+](https://nodejs.org/) and
90
+ [Node.js 24+](https://nodejs.org/) and
91
91
  [Docker](https://www.docker.com/). Docker carries the browser, Android, and
92
92
  WASI toolchains and is pulled automatically on the first build. iOS additionally
93
93
  requires macOS, CMake 3.28+, Xcode, and CocoaPods.
package/package.json CHANGED
@@ -1,55 +1,52 @@
1
1
  {
2
- "name": "crossbind",
3
- "version": "2.0.0-beta.53",
4
- "license": "MIT",
5
- "homepage": "https://crossbind.dev",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/crossbind/crossbind.git"
9
- },
10
- "description": "Bind C++ to JavaScript without extra code. crossbind supports WebAssembly and React Native for cross-platform compatibility.",
11
- "type": "module",
12
- "engines": {
13
- "node": ">=20"
14
- },
15
- "bin": {
16
- "crossbind": "./src/bin.js"
17
- },
18
- "main": "src/index.js",
19
- "devDependencies": {
20
- "@vitest/coverage-v8": "^4.1.10",
21
- "vitest": "^4.1.10"
22
- },
23
- "dependencies": {
24
- "@rollup/plugin-commonjs": "^29.0.3",
25
- "@rollup/plugin-json": "^6.1.0",
26
- "@rollup/plugin-node-resolve": "^16.0.3",
27
- "@rollup/plugin-virtual": "^3.0.2",
28
- "comlink": "^4.4.2",
29
- "commander": "^15.0.0",
30
- "glob": "^13.0.6",
31
- "log-update": "^8.0.0",
32
- "picocolors": "^1.1.1",
33
- "replace": "^1.2.2",
34
- "rollup": "^4.62.4",
35
- "spdx-expression-parse": "^5.0.0",
36
- "upath": "^3.0.8"
37
- },
38
- "keywords": [
39
- "webassembly",
40
- "react-native",
41
- "bind",
42
- "c++",
43
- "emscripten",
44
- "embind",
45
- "swig",
46
- "android",
47
- "ios",
48
- "mobile"
49
- ],
50
- "scripts": {
51
- "test": "vitest run",
52
- "test:watch": "vitest",
53
- "test:coverage": "vitest run --coverage"
54
- }
55
- }
2
+ "name": "crossbind",
3
+ "version": "2.0.0-beta.56",
4
+ "license": "MIT",
5
+ "homepage": "https://crossbind.dev",
6
+ "repository": "https://github.com/crossbind/crossbind.git",
7
+ "description": "Bind C++ to JavaScript without extra code. crossbind supports WebAssembly and React Native for cross-platform compatibility.",
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=24"
11
+ },
12
+ "bin": {
13
+ "crossbind": "./src/bin.js"
14
+ },
15
+ "main": "src/index.js",
16
+ "scripts": {
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:coverage": "vitest run --coverage"
20
+ },
21
+ "devDependencies": {
22
+ "@vitest/coverage-v8": "^4.1.10",
23
+ "vitest": "^4.1.10"
24
+ },
25
+ "dependencies": {
26
+ "@rollup/plugin-commonjs": "^29.0.3",
27
+ "@rollup/plugin-json": "^6.1.0",
28
+ "@rollup/plugin-node-resolve": "^16.0.3",
29
+ "@rollup/plugin-virtual": "^3.0.2",
30
+ "comlink": "^4.4.2",
31
+ "commander": "^15.0.0",
32
+ "glob": "^13.0.6",
33
+ "log-update": "^8.0.0",
34
+ "picocolors": "^1.1.1",
35
+ "replace": "^1.2.2",
36
+ "rollup": "^4.62.4",
37
+ "spdx-expression-parse": "^5.0.0",
38
+ "upath": "^3.0.8"
39
+ },
40
+ "keywords": [
41
+ "webassembly",
42
+ "react-native",
43
+ "bind",
44
+ "c++",
45
+ "emscripten",
46
+ "embind",
47
+ "swig",
48
+ "android",
49
+ "ios",
50
+ "mobile"
51
+ ]
52
+ }
Binary file
@@ -169,19 +169,19 @@ export default async function buildWasm(target, options = {}) {
169
169
  triggerExtensions('buildWasm', 'beforeBuildBrowser', [emccFlags]);
170
170
 
171
171
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
172
- run('emcc', [
172
+ run('em++', [
173
173
  '-lembind',
174
174
  ...emccFlags,
175
175
  // '-lwebsocket.js', '-sPROXY_POSIX_SOCKETS', '-sWEBSOCKET_DEBUG=1', '-sJSPI', '-g', '-sWASMFS',
176
- '-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
176
+ '-s', 'FORCE_FILESYSTEM=1',
177
177
  '-sEXPORT_NAME=Module2', // '-pthread', '-sPTHREAD_POOL_SIZE=5',
178
178
  ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/browser.cpp`,
179
179
  ...(isProd ? ['-O3'] : []),
180
180
  '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
181
181
  '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
182
182
  '-s', 'ALLOW_MEMORY_GROWTH=1',
183
- // emsdk 6 defaults GROWABLE_ARRAYBUFFERS=1; Firefox/WebKit TextDecoder rejects views
184
- // over resizable ArrayBuffers, breaking every string crossing the wasm boundary.
183
+ // Keep this explicit: Firefox/WebKit TextDecoder rejects views over resizable
184
+ // ArrayBuffers, breaking every string crossing the wasm boundary.
185
185
  '-s', 'GROWABLE_ARRAYBUFFERS=0',
186
186
  '-s', 'WASMFS',
187
187
  '-s', 'ENVIRONMENT=web,webview,worker',
@@ -240,10 +240,9 @@ export default async function buildWasm(target, options = {}) {
240
240
  triggerExtensions('buildWasm', 'beforeBuildEdge', [emccFlags]);
241
241
 
242
242
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
243
- run('emcc', [
243
+ run('em++', [
244
244
  '-lembind',
245
245
  ...emccFlags,
246
- '-sWASM_BIGINT=1',
247
246
  '-sEXPORT_NAME=Module2',
248
247
  // rustSources carries the embind-rust adapter TU (emval/json/tid hooks); without
249
248
  // it any linked Rust package archive fails with undefined crossbind_* symbols.
@@ -273,11 +272,11 @@ export default async function buildWasm(target, options = {}) {
273
272
 
274
273
  triggerExtensions('buildWasm', 'beforeBuildNodeJS', [emccFlags]);
275
274
 
276
- run('emcc', [
275
+ run('em++', [
277
276
  '-lembind',
278
277
  ...emccFlags,
279
- // '-s', 'FETCH', '-sJSPI', '-sWASM_BIGINT=1', '-pthread', '-sPTHREAD_POOL_SIZE=5',
280
- '-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
278
+ // '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
279
+ '-s', 'FORCE_FILESYSTEM=1',
281
280
  ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
282
281
  ...(isProd ? ['-O3'] : []),
283
282
  '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
@@ -203,7 +203,7 @@ export default function createLib(target, fileType, options = {}) {
203
203
  stageWasiStubs();
204
204
  }
205
205
  run(null, [
206
- './configure',
206
+ state.config.build?.configureProgram || './configure',
207
207
  ...buildParams,
208
208
  `--prefix=${libdir}`,
209
209
  ], platformPrefix, target, buildEnv);
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from '../utils/pullDockerImage.js';
4
4
  import getOsUserAndGroupId from '../utils/getOsUserAndGroupId.js';
5
5
  import replaceBasePathForDockerUtil from '../utils/replaceBasePathForDocker.js';
6
+ import { DOCKER_RUN_SECURITY_ARGS } from '../utils/dockerSecurity.js';
6
7
  import state from '../state/index.js';
7
8
  import { wasiCFlags, wasiCxxFlags, resolveWasiSdkPath, WASI_TARGET_TRIPLE } from '../utils/wasiToolchain.js';
8
9
 
@@ -20,51 +21,70 @@ const iosSdkPath = `${iOSDevPath}/Platforms/iPhoneOS.platform/Developer/SDKs/iPh
20
21
  const iosSimSdkPath = `${iOSDevPath}/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk`;
21
22
 
22
23
  const androidParamsArm64 = [
23
- '-e', `AR=${t}/llvm-ar`,
24
- '-e', `AS=${t}/llvm-as`,
25
- '-e', `CC=${t}/${CROSSCOMPILER_ARM64}-clang`,
26
- '-e', `CXX=${t}/${CROSSCOMPILER_ARM64}-clang++`,
27
- '-e', `LD=${t}/ld`,
28
- '-e', `RANLIB=${t}/llvm-ranlib`,
29
- '-e', `STRIP=${t}/llvm-strip`,
30
- '-e', `NM=${t}/nm`,
31
- '-e', `CFLAGS=--sysroot=${t2}/sysroot`,
24
+ '-e',
25
+ `AR=${t}/llvm-ar`,
26
+ '-e',
27
+ `AS=${t}/llvm-as`,
28
+ '-e',
29
+ `CC=${t}/${CROSSCOMPILER_ARM64}-clang`,
30
+ '-e',
31
+ `CXX=${t}/${CROSSCOMPILER_ARM64}-clang++`,
32
+ '-e',
33
+ `LD=${t}/ld`,
34
+ '-e',
35
+ `RANLIB=${t}/llvm-ranlib`,
36
+ '-e',
37
+ `STRIP=${t}/llvm-strip`,
38
+ '-e',
39
+ `NM=${t}/nm`,
40
+ '-e',
41
+ `CFLAGS=--sysroot=${t2}/sysroot`,
32
42
  ];
33
43
 
34
44
  const androidParamsX86_64 = [
35
- '-e', `AR=${t}/llvm-ar`,
36
- '-e', `AS=${t}/llvm-as`,
37
- '-e', `CC=${t}/${CROSSCOMPILER_x86_64}-clang`,
38
- '-e', `CXX=${t}/${CROSSCOMPILER_x86_64}-clang++`,
39
- '-e', `LD=${t}/ld`,
40
- '-e', `RANLIB=${t}/llvm-ranlib`,
41
- '-e', `STRIP=${t}/llvm-strip`,
42
- '-e', `NM=${t}/nm`,
43
- '-e', `CFLAGS=--sysroot=${t2}/sysroot`,
45
+ '-e',
46
+ `AR=${t}/llvm-ar`,
47
+ '-e',
48
+ `AS=${t}/llvm-as`,
49
+ '-e',
50
+ `CC=${t}/${CROSSCOMPILER_x86_64}-clang`,
51
+ '-e',
52
+ `CXX=${t}/${CROSSCOMPILER_x86_64}-clang++`,
53
+ '-e',
54
+ `LD=${t}/ld`,
55
+ '-e',
56
+ `RANLIB=${t}/llvm-ranlib`,
57
+ '-e',
58
+ `STRIP=${t}/llvm-strip`,
59
+ '-e',
60
+ `NM=${t}/nm`,
61
+ '-e',
62
+ `CFLAGS=--sysroot=${t2}/sysroot`,
44
63
  ];
45
64
 
46
65
  const IOS_HOST_FLAGS = `-arch arm64 -isysroot ${iosSdkPath} -fembed-bitcode`;
47
66
  const IOS_SIM_HOST_FLAGS = `-arch arm64 -isysroot ${iosSimSdkPath} -fembed-bitcode`;
48
- const IOS_IPHONE_PARAMS = [
49
- '-e', `CFLAGS="${IOS_HOST_FLAGS}"`,
50
- '-e', `CXXFLAGS="${IOS_HOST_FLAGS}"`,
51
- '-e', `LDFLAGS="${IOS_HOST_FLAGS}"`,
52
- ];
53
- const IOS_SIM_PARAMS = [
54
- '-e', `CFLAGS="${IOS_SIM_HOST_FLAGS}"`,
55
- '-e', `CXXFLAGS="${IOS_SIM_HOST_FLAGS}"`,
56
- '-e', `LDFLAGS="${IOS_SIM_HOST_FLAGS}"`,
57
- ];
67
+ const IOS_IPHONE_PARAMS = ['-e', `CFLAGS="${IOS_HOST_FLAGS}"`, '-e', `CXXFLAGS="${IOS_HOST_FLAGS}"`, '-e', `LDFLAGS="${IOS_HOST_FLAGS}"`];
68
+ const IOS_SIM_PARAMS = ['-e', `CFLAGS="${IOS_SIM_HOST_FLAGS}"`, '-e', `CXXFLAGS="${IOS_SIM_HOST_FLAGS}"`, '-e', `LDFLAGS="${IOS_SIM_HOST_FLAGS}"`];
58
69
  const iosParams = [
59
- '-e', `AR=${iosBinPath}/ar`,
60
- '-e', `AS=${iosBinPath}/as`,
61
- '-e', `CC=${iosBinPath}/clang`,
62
- '-e', `CXX=${iosBinPath}/clang++`,
63
- '-e', `CPP=${iosBinPath}/cpp`,
64
- '-e', `LD=${iosBinPath}/ld`,
65
- '-e', `RANLIB=${iosBinPath}/ranlib`,
66
- '-e', `STRIP=${iosBinPath}/strip`,
67
- '-e', `NM=${iosBinPath}/llvm-nm`,
70
+ '-e',
71
+ `AR=${iosBinPath}/ar`,
72
+ '-e',
73
+ `AS=${iosBinPath}/as`,
74
+ '-e',
75
+ `CC=${iosBinPath}/clang`,
76
+ '-e',
77
+ `CXX=${iosBinPath}/clang++`,
78
+ '-e',
79
+ `CPP=${iosBinPath}/cpp`,
80
+ '-e',
81
+ `LD=${iosBinPath}/ld`,
82
+ '-e',
83
+ `RANLIB=${iosBinPath}/ranlib`,
84
+ '-e',
85
+ `STRIP=${iosBinPath}/strip`,
86
+ '-e',
87
+ `NM=${iosBinPath}/llvm-nm`,
68
88
  ];
69
89
 
70
90
  /* const iosMetalParams = [
@@ -103,7 +123,7 @@ export default function run(program, params = [], platformPrefix = null, target
103
123
  switch (target.platform) {
104
124
  case 'wasm':
105
125
  platformParams = ['-e', 'CXXFLAGS=-fwasm-exceptions', '-e', 'CFLAGS=-fwasm-exceptions'];
106
- if (params[0].includes('configure')) dProgram = 'emconfigure';
126
+ if (params[0].toLowerCase().includes('configure')) dProgram = 'emconfigure';
107
127
  else if (params[0] === 'make') dProgram = 'emmake';
108
128
  else if (params[0] === 'cmake') dProgram = 'emcmake';
109
129
  else if (params[0] === 'cc') dProgram = 'emcc';
@@ -111,23 +131,29 @@ export default function run(program, params = [], platformPrefix = null, target
111
131
  case 'wasi': {
112
132
  const wasiSdk = wasiHostSdk || '/opt/wasi-sdk';
113
133
  if (wasiHostSdk && !fs.existsSync(`${wasiHostSdk}/share/wasi-sysroot/lib/${WASI_TARGET_TRIPLE}`)) {
114
- throw new Error(`crossbind: the wasi-sdk at ${wasiHostSdk} has no ${WASI_TARGET_TRIPLE} sysroot. platform:'wasi' targets WASI 0.3 (p3) - upgrade to wasi-sdk >= 34.`);
134
+ throw new Error(
135
+ `crossbind: the wasi-sdk at ${wasiHostSdk} has no ${WASI_TARGET_TRIPLE} sysroot. platform:'wasi' targets WASI 0.3 (p3) - upgrade to wasi-sdk >= 34.`,
136
+ );
115
137
  }
116
138
  [dProgram, ...dParams] = params;
117
139
  platformParams = [
118
- '-e', `CC=${wasiSdk}/bin/clang`,
119
- '-e', `CXX=${wasiSdk}/bin/clang++`,
120
- '-e', `AR=${wasiSdk}/bin/ar`,
121
- '-e', `RANLIB=${wasiSdk}/bin/ranlib`,
122
- '-e', `NM=${wasiSdk}/bin/nm`,
123
- '-e', `CFLAGS=${wasiCFlags().join(' ')}`,
124
- '-e', `CXXFLAGS=${wasiCxxFlags().join(' ')}`,
140
+ '-e',
141
+ `CC=${wasiSdk}/bin/clang`,
142
+ '-e',
143
+ `CXX=${wasiSdk}/bin/clang++`,
144
+ '-e',
145
+ `AR=${wasiSdk}/bin/ar`,
146
+ '-e',
147
+ `RANLIB=${wasiSdk}/bin/ranlib`,
148
+ '-e',
149
+ `NM=${wasiSdk}/bin/nm`,
150
+ '-e',
151
+ `CFLAGS=${wasiCFlags().join(' ')}`,
152
+ '-e',
153
+ `CXXFLAGS=${wasiCxxFlags().join(' ')}`,
125
154
  ];
126
155
  if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
127
- dParams = [
128
- ...dParams,
129
- `-DCMAKE_TOOLCHAIN_FILE=${wasiSdk}/share/cmake/wasi-sdk-p3.cmake`,
130
- ];
156
+ dParams = [...dParams, `-DCMAKE_TOOLCHAIN_FILE=${wasiSdk}/share/cmake/wasi-sdk-p3.cmake`];
131
157
  } else if (dProgram === 'wasi-clang++') {
132
158
  dProgram = `${wasiSdk}/bin/clang++`;
133
159
  platformParams = [];
@@ -158,7 +184,8 @@ export default function run(program, params = [], platformPrefix = null, target
158
184
  if (dParams[0] !== '--build' && dParams[0] !== '--install') {
159
185
  dParams = [
160
186
  ...dParams,
161
- '-G', 'Xcode',
187
+ '-G',
188
+ 'Xcode',
162
189
  '-DBUILD_SHARED_LIBS=OFF',
163
190
  '-DFRAMEWORK=TRUE',
164
191
  '-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0',
@@ -170,7 +197,7 @@ export default function run(program, params = [], platformPrefix = null, target
170
197
  '-DCMAKE_SYSTEM_PROCESSOR=arm64',
171
198
  `-DCMAKE_C_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
172
199
  `-DCMAKE_CXX_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
173
- '-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY=\'iPhone Developer\'',
200
+ "-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY='iPhone Developer'",
174
201
  `-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=${state.config.system.XCODE_DEVELOPMENT_TEAM}`,
175
202
  ];
176
203
  }
@@ -195,7 +222,7 @@ export default function run(program, params = [], platformPrefix = null, target
195
222
  '-DCMAKE_SYSTEM_PROCESSOR=arm64',
196
223
  `-DCMAKE_C_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
197
224
  `-DCMAKE_CXX_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
198
- '-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY=\'iPhone Developer\'',
225
+ "-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY='iPhone Developer'",
199
226
  `-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=${state.config.system.XCODE_DEVELOPMENT_TEAM}`,
200
227
  ];
201
228
  }
@@ -207,14 +234,25 @@ export default function run(program, params = [], platformPrefix = null, target
207
234
 
208
235
  const env = {};
209
236
  let runner = 'DOCKER';
210
- if (((target?.platform === 'ios' || (target?.platform === 'wasi' && wasiHostSdk)) && program === null) || state.config.system.RUNNER === 'LOCAL') {
237
+ if (
238
+ ((target?.platform === 'ios' || (target?.platform === 'wasi' && wasiHostSdk)) && program === null) ||
239
+ state.config.system.RUNNER === 'LOCAL'
240
+ ) {
211
241
  runner = 'LOCAL';
212
242
  }
213
243
 
214
244
  if (runner === 'LOCAL') {
215
245
  const allowedEnv = [
216
- '^PWD$', '^SHELL$', '^LC_CTYPE$', '^PATH$', '^HOME$', '^TMPDIR$', '^USER$',
217
- '^PODS_*', '^CONFIGURATION_BUILD_DIR$', '^UNLOCALIZED_RESOURCES_FOLDER_PATH$',
246
+ '^PWD$',
247
+ '^SHELL$',
248
+ '^LC_CTYPE$',
249
+ '^PATH$',
250
+ '^HOME$',
251
+ '^TMPDIR$',
252
+ '^USER$',
253
+ '^PODS_*',
254
+ '^CONFIGURATION_BUILD_DIR$',
255
+ '^UNLOCALIZED_RESOURCES_FOLDER_PATH$',
218
256
  ];
219
257
  Object.entries(process.env).forEach(([key, value]) => {
220
258
  if (allowedEnv.some((e) => key.match(e))) {
@@ -230,7 +268,7 @@ export default function run(program, params = [], platformPrefix = null, target
230
268
  const value = rest.join('=');
231
269
  if (['CFLAGS', 'CXXFLAGS', 'CPPFLAGS', 'LDFLAGS'].includes(key)) {
232
270
  let v = value;
233
- if (v.startsWith('\'') || v.startsWith('"')) {
271
+ if (v.startsWith("'") || v.startsWith('"')) {
234
272
  v = v.substring(1, v.length - 1);
235
273
  }
236
274
  if (env[key]) env[key] += ` ${v}`;
@@ -264,7 +302,7 @@ export default function run(program, params = [], platformPrefix = null, target
264
302
  const role = imageRoleFor(target);
265
303
  if (state.config.system.RUNNER === 'DOCKER_RUN') {
266
304
  imageOrContainer = getDockerImage(role);
267
- runnerParams = ['run', '--rm', '-v', `${state.config.paths.base}:/tmp/crossbind/live`];
305
+ runnerParams = ['run', '--rm', ...DOCKER_RUN_SECURITY_ARGS, '-v', `${state.config.paths.base}:/tmp/crossbind/live`];
268
306
  if (target?.platform === 'android') {
269
307
  // Google ships the linux NDK for x86_64 only; use the amd64 leaf ref (classic store holds one platform per digest).
270
308
  pullDockerImage(role, 'linux/amd64');
@@ -280,8 +318,10 @@ export default function run(program, params = [], platformPrefix = null, target
280
318
 
281
319
  const args = [
282
320
  ...runnerParams,
283
- '--user', getOsUserAndGroupId(),
284
- '--workdir', replaceBasePathForDocker(dockerOptions.workdir || buildPath),
321
+ '--user',
322
+ getOsUserAndGroupId(),
323
+ '--workdir',
324
+ replaceBasePathForDocker(dockerOptions.workdir || buildPath),
285
325
  ...replaceBasePathForDocker(dockerEnv),
286
326
  // '-e', replaceBasePathForDocker(`CCACHE_DIR=${state.config.paths.build}/ccache`),
287
327
  imageOrContainer,
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": "1.0.3",
3
+ "registry": "ghcr.io/crossbind",
4
+ "toolchains": {
5
+ "rust": "1.98.1"
6
+ },
7
+ "images": {
8
+ "rust-sysroot": {
9
+ "index": "sha256:6a5db2ecf54398d837fdbbd8c1f2cdaafd14fd486ac3af4206d21bc9fe66e923",
10
+ "platforms": {
11
+ "linux/arm64": "sha256:b5dbd8b4f233f5d6c0cfe7554b95b412e0e3396d4d4847facf116c155251b0b7",
12
+ "linux/amd64": "sha256:1989d0ab32d2372a2086efe50e33dbb861fa903e451b891f30004e0f172a1a9a"
13
+ }
14
+ },
15
+ "base": {
16
+ "index": "sha256:c9b501dca53bc2e51dddd1af64ede75220792e66a47ed488ad6172b58bee1b37",
17
+ "platforms": {
18
+ "linux/arm64": "sha256:edf01f0c4d7fc551e18775e76b2a6c2b9b54075b59f21f7635ccecaebcc6c282",
19
+ "linux/amd64": "sha256:debea6ee164eeba49155ad59365313a9dd291491b3339491a780b46d1b5b96f4"
20
+ }
21
+ },
22
+ "web": {
23
+ "index": "sha256:633da840658f12f291c17c3305adf265c7d52307b3b61b84156d5f85f3121779",
24
+ "platforms": {
25
+ "linux/arm64": "sha256:a5fe9f3693d789f04490963afbe80fa71a8ff0d0cc6e442b76ca37492fbf3221",
26
+ "linux/amd64": "sha256:65abff12143740d4557e9cbfd923b6fd6c5d01f25e96a1d27bbad5e083bb9f74"
27
+ }
28
+ },
29
+ "android": {
30
+ "index": "sha256:1f415db57ccb57c8850ad4498d9fead35f5104099fd355c067208b2e53c25b23",
31
+ "platforms": {
32
+ "linux/amd64": "sha256:38d7a10c233cfab13adb110383e386e2a649364a8285d6b3ad0696e1e343b58a"
33
+ }
34
+ }
35
+ }
36
+ }
@@ -74,3 +74,8 @@ export function renderDispatcher(bin) {
74
74
  '',
75
75
  ].join('\n');
76
76
  }
77
+
78
+ // Under the target build tree, never os.tmpdir(): the containerized compiler only sees paths.base.
79
+ export function multitoolWorkRoot(buildPath, platformPrefix, targetPath) {
80
+ return `${buildPath}/${platformPrefix}/${targetPath}`;
81
+ }
@@ -0,0 +1,4 @@
1
+ // Compilation executes project build scripts and third-party toolchains. These flags do not turn
2
+ // Docker into a sandbox, but they remove privileges a compiler does not need. Keep the list shared
3
+ // so the generic command runner and the dedicated cargo runner cannot drift.
4
+ export const DOCKER_RUN_SECURITY_ARGS = Object.freeze(['--cap-drop', 'ALL', '--security-opt', 'no-new-privileges=true']);
@@ -1,7 +1,7 @@
1
1
  import { getContentHash } from './hash.js';
2
2
 
3
3
  const REPLACE_RECIPE_KEYS = [
4
- 'buildType', 'getURL', 'env', 'copyToSource', 'copyToDist',
4
+ 'buildType', 'configureProgram', 'getURL', 'env', 'copyToSource', 'copyToDist',
5
5
  'beforeRun', 'useIOSCMake', 'sourceReplaceList', 'getExtraLibs',
6
6
  ];
7
7
 
@@ -1,27 +1,21 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
2
3
  import { getContentHash } from './hash.js';
3
4
 
4
- // Digest-pinned so a re-pushed tag can't swap the compiler; IMAGE_VERSION only names containers
5
- // and records which release these digests came from - bump every pin via scripts/pin-docker-image.js.
6
- const REGISTRY = 'ghcr.io/crossbind';
7
- const IMAGE_VERSION = '1.0.2';
8
-
9
- // One entry per role in the image family. `ref` is the multi-arch index, which a native pull
10
- // resolves per platform; `amd64` is that index's linux/amd64 leaf, needed wherever the CLI forces
11
- // a platform, because the classic image store holds one platform per digest reference. The android
12
- // image is amd64-only - Google ships the linux NDK host tools for x86_64 alone - so its index
13
- // carries a single leaf and both of its refs resolve to the same platform.
14
- // Generated by scripts/pin-docker-image.js from a release digest table; do not edit by hand.
15
- const IMAGES = {
16
- web: {
17
- ref: `${REGISTRY}/web@sha256:c1d1d81a4775871cd90c8ac980e0a5f87cb7890822117f77be1c8345f51b1f47`,
18
- amd64: `${REGISTRY}/web@sha256:9010948430c95120b8c19f73377f8684bc6b54e03537790f465237225e48dae2`,
19
- },
20
- android: {
21
- ref: `${REGISTRY}/android@sha256:a3facbf2181f568f12a9fe89054188895665a63e2c147b790fd7e25f3e128ac8`,
22
- amd64: `${REGISTRY}/android@sha256:4694c6210379a87d8cf0e7f8b56a76a9c74a8724a1aff037c943dcdb521f4de1`,
23
- },
24
- };
5
+ // This committed JSON is the one digest table for the CLI, release manifests and release assets.
6
+ // scripts/pin-docker-image.js replaces it from the image workflow's verified `digests` artifact.
7
+ const table = createRequire(import.meta.url)('../assets/toolchain-digests.json');
8
+ const REGISTRY = table.registry;
9
+ const IMAGE_VERSION = table.version;
10
+ const IMAGES = Object.fromEntries(
11
+ ['web', 'android'].map((role) => [
12
+ role,
13
+ {
14
+ ref: `${REGISTRY}/${role}@${table.images[role].index}`,
15
+ amd64: `${REGISTRY}/${role}@${table.images[role].platforms['linux/amd64']}`,
16
+ },
17
+ ]),
18
+ );
25
19
 
26
20
  const OVERRIDE_KEYS = { web: 'CROSSBIND_IMAGE_WEB', android: 'CROSSBIND_IMAGE_ANDROID' };
27
21
 
@@ -6,6 +6,7 @@ import state from '../state/index.js';
6
6
  import getOsUserAndGroupId from './getOsUserAndGroupId.js';
7
7
  import replaceBasePathForDockerUtil, { DOCKER_BASE } from './replaceBasePathForDocker.js';
8
8
  import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
9
+ import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
9
10
 
10
11
  // Every cargo invocation crossbind makes goes through here.
11
12
  //
@@ -28,14 +29,26 @@ const RUSTFLAGS_SEPARATOR = String.fromCharCode(0x1f);
28
29
  // the compiler - RUSTC_WRAPPER, CARGO_BUILD_RUSTC, CARGO_TARGET_<TRIPLE>_LINKER, RUSTFLAGS,
29
30
  // RUSTUP_TOOLCHAIN, RUSTC_BOOTSTRAP - cannot reach cargo from the caller's environment.
30
31
  const ALLOWED_ENV = [
31
- /^PATH$/, /^HOME$/, /^USER$/, /^LOGNAME$/, /^SHELL$/, /^TERM$/,
32
- /^TMPDIR$/, /^TEMP$/, /^TMP$/,
33
- /^LANG$/, /^LC_[A-Z_]+$/,
32
+ /^PATH$/,
33
+ /^HOME$/,
34
+ /^USER$/,
35
+ /^LOGNAME$/,
36
+ /^SHELL$/,
37
+ /^TERM$/,
38
+ /^TMPDIR$/,
39
+ /^TEMP$/,
40
+ /^TMP$/,
41
+ /^LANG$/,
42
+ /^LC_[A-Z_]+$/,
34
43
  /^RUSTUP_HOME$/,
35
44
  /^SSH_AUTH_SOCK$/, // git dependencies over ssh
36
- /^CARGO_NET_/, /^CARGO_REGISTRIES_/, // offline/vendored builds and private registries
45
+ /^CARGO_NET_/,
46
+ /^CARGO_REGISTRIES_/, // offline/vendored builds and private registries
37
47
  /^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
38
- /^SYSTEMROOT$/i, /^WINDIR$/i, /^USERPROFILE$/i, /^PROGRAMDATA$/i, // windows cannot spawn without these
48
+ /^SYSTEMROOT$/i,
49
+ /^WINDIR$/i,
50
+ /^USERPROFILE$/i,
51
+ /^PROGRAMDATA$/i, // windows cannot spawn without these
39
52
  ];
40
53
 
41
54
  // Recent cargo applies config.toml only, but the extensionless name is still read by older
@@ -76,14 +89,18 @@ export function assertCleanConfigChain(home, cwd) {
76
89
  for (const name of CONFIG_NAMES) {
77
90
  const file = path.join(home, name);
78
91
  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.`);
92
+ throw new Error(
93
+ `crossbind: unexpected cargo config at ${file} - crossbind owns this CARGO_HOME and never writes one. Remove it and build again.`,
94
+ );
80
95
  }
81
96
  }
82
97
  for (let dir = cwd; ; dir = path.dirname(dir)) {
83
98
  for (const name of CONFIG_NAMES) {
84
99
  const file = path.join(dir, '.cargo', name);
85
100
  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.`);
101
+ throw new Error(
102
+ `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.`,
103
+ );
87
104
  }
88
105
  }
89
106
  if (path.dirname(dir) === dir) return;
@@ -106,9 +123,7 @@ function allowedEnv() {
106
123
  return env;
107
124
  }
108
125
 
109
- function cargoEnv({
110
- home, rustflags, panic, allowUnstable,
111
- }) {
126
+ function cargoEnv({ home, rustflags, panic, allowUnstable }) {
112
127
  const env = { CARGO_HOME: home };
113
128
  // Depth defence: -Z fails with E0554 instead of quietly enabling unstable behaviour. The value
114
129
  // -1 disables unstable features on EVERY channel, nightly included, so the one caller that
@@ -131,6 +146,7 @@ function createHint(name, base, home, image) {
131
146
  return [
132
147
  'Create it with:',
133
148
  ` docker run -d --name ${name} \\`,
149
+ ` ${DOCKER_RUN_SECURITY_ARGS.join(' ')} \\`,
134
150
  ` -v ${base}:${DOCKER_BASE} \\`,
135
151
  ` -v ${home}:${CONTAINER_CARGO_HOME} \\`,
136
152
  ` ${image} sleep infinity`,
@@ -138,7 +154,13 @@ function createHint(name, base, home, image) {
138
154
  }
139
155
 
140
156
  function samePath(a, b) {
141
- const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
157
+ const real = (p) => {
158
+ try {
159
+ return fs.realpathSync(p);
160
+ } catch {
161
+ return path.resolve(p);
162
+ }
163
+ };
142
164
  return real(a) === real(b);
143
165
  }
144
166
 
@@ -152,13 +174,20 @@ function assertExecContainer(name, base, home, image) {
152
174
  throw new Error(`crossbind: RUNNER=DOCKER_EXEC needs a container named '${name}', which does not exist.\n${hint}`);
153
175
  }
154
176
  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}`);
177
+ throw new Error(
178
+ `crossbind: the container '${name}' exists but is not running. Start it with \`docker start ${name}\`, or recreate it.\n${hint}`,
179
+ );
156
180
  }
157
- for (const [destination, source] of [[DOCKER_BASE, base], [CONTAINER_CARGO_HOME, home]]) {
181
+ for (const [destination, source] of [
182
+ [DOCKER_BASE, base],
183
+ [CONTAINER_CARGO_HOME, home],
184
+ ]) {
158
185
  const mount = (info.Mounts ?? []).find((m) => m.Destination === destination);
159
186
  if (!mount || !samePath(mount.Source, source)) {
160
187
  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}`);
188
+ throw new Error(
189
+ `crossbind: the container '${name}' does not mount ${source} at ${destination}${found}. It predates the cargo mount, so recreate it.\n${hint}`,
190
+ );
162
191
  }
163
192
  }
164
193
  // `docker run --workdir` creates the directory; `docker exec --workdir` does not, and cargo
@@ -170,9 +199,7 @@ function assertExecContainer(name, base, home, image) {
170
199
  }
171
200
  }
172
201
 
173
- export default function runCargo(args, {
174
- cwd, rustflags = [], panic, capture = false, maxBuffer, allowUnstable = false, target,
175
- } = {}) {
202
+ export default function runCargo(args, { cwd, rustflags = [], panic, capture = false, maxBuffer, allowUnstable = false, target } = {}) {
176
203
  const home = cargoHome();
177
204
  const workdir = cwd ?? neutralCwd();
178
205
  fs.mkdirSync(home, { recursive: true });
@@ -200,7 +227,10 @@ export default function runCargo(args, {
200
227
  // Only what cargo needs travels in: the image already carries PATH, RUSTUP_HOME and the
201
228
  // toolchain, and the host's PATH would be meaningless there.
202
229
  const env = cargoEnv({
203
- home: CONTAINER_CARGO_HOME, rustflags, panic, allowUnstable,
230
+ home: CONTAINER_CARGO_HOME,
231
+ rustflags,
232
+ panic,
233
+ allowUnstable,
204
234
  });
205
235
  const envArgs = Object.entries(env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
206
236
 
@@ -208,20 +238,25 @@ export default function runCargo(args, {
208
238
  if (runner === 'DOCKER_EXEC') {
209
239
  const name = getDockerContainerName(base, role);
210
240
  assertExecContainer(name, base, home, getDockerImage(role, platform));
211
- runnerArgs = ['exec', ...envArgs,
212
- '--user', getOsUserAndGroupId(),
213
- '--workdir', CONTAINER_CWD,
214
- name];
241
+ runnerArgs = ['exec', ...envArgs, '--user', getOsUserAndGroupId(), '--workdir', CONTAINER_CWD, name];
215
242
  } else {
216
243
  pullDockerImage(role, platform);
217
- runnerArgs = ['run', '--rm',
244
+ runnerArgs = [
245
+ 'run',
246
+ '--rm',
218
247
  ...(platform ? ['--platform', platform] : []),
219
- '-v', `${base}:${DOCKER_BASE}`,
220
- '-v', `${home}:${CONTAINER_CARGO_HOME}`,
248
+ ...DOCKER_RUN_SECURITY_ARGS,
249
+ '-v',
250
+ `${base}:${DOCKER_BASE}`,
251
+ '-v',
252
+ `${home}:${CONTAINER_CARGO_HOME}`,
221
253
  ...envArgs,
222
- '--user', getOsUserAndGroupId(),
223
- '--workdir', CONTAINER_CWD,
224
- getDockerImage(role, platform)];
254
+ '--user',
255
+ getOsUserAndGroupId(),
256
+ '--workdir',
257
+ CONTAINER_CWD,
258
+ getDockerImage(role, platform),
259
+ ];
225
260
  }
226
261
 
227
262
  return spawnSync('docker', [...runnerArgs, 'cargo', ...replaceBasePathForDockerUtil(args, base)], options);
@@ -1,11 +1,14 @@
1
- // Generated by scripts/pin-rust-sysroot.js - do not edit by hand.
2
- //
3
1
  // The published sysroot image a host build (RUNNER=LOCAL) reads its Rust sysroots out of - the same
4
2
  // object the web image embeds, not a repackaged copy, so the two channels cannot drift. The index
5
3
  // digest is the whole contract: the loader verifies the index body against it, then each descriptor
6
4
  // inside a verified body authenticates the next fetch, down to the layer.
5
+ import { createRequire } from 'node:module';
6
+
7
+ const table = createRequire(import.meta.url)('../assets/toolchain-digests.json');
8
+ const sysroot = table.images['rust-sysroot'];
9
+
7
10
  export default {
8
- version: '1.0.2',
9
- image: 'ghcr.io/crossbind/rust-sysroot',
10
- index: 'sha256:0ab2e522b1b09b2af7f312d4b940ab92ee470e8165df4b563e274a4eb2fdb903',
11
+ version: table.version,
12
+ image: `${table.registry}/rust-sysroot`,
13
+ index: sysroot.index,
11
14
  };