crossbind 2.0.0-beta.63 → 2.0.0-beta.65

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 (77) hide show
  1. package/AGENTS.md +2 -1
  2. package/package.json +2 -2
  3. package/src/actions/androidDevice.js +77 -0
  4. package/src/actions/androidGradle.js +121 -0
  5. package/src/actions/buildApp.js +434 -0
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildDependencies.js +4 -3
  8. package/src/actions/buildExternal.js +3 -3
  9. package/src/actions/buildJs.js +2 -9
  10. package/src/actions/buildLib.js +1 -1
  11. package/src/actions/buildNode.js +19 -16
  12. package/src/actions/buildWasiCommand.js +10 -5
  13. package/src/actions/buildWasm.js +24 -3
  14. package/src/actions/createImportedBridges.js +7 -0
  15. package/src/actions/createInterface.js +4 -5
  16. package/src/actions/createLib.js +17 -2
  17. package/src/actions/getCmakeParameters.js +2 -1
  18. package/src/actions/getData.js +2 -1
  19. package/src/actions/getDependLibs.js +8 -2
  20. package/src/actions/getEnv.js +7 -0
  21. package/src/actions/getLinkInputs.js +10 -7
  22. package/src/actions/gradleProject.js +110 -0
  23. package/src/actions/iosSimulator.js +63 -0
  24. package/src/actions/licenses.js +160 -14
  25. package/src/actions/nativeBridges.js +35 -0
  26. package/src/actions/run.js +3 -3
  27. package/src/actions/runnerCommands.js +16 -4
  28. package/src/actions/target.js +2 -1
  29. package/src/actions/writeRuntimeEntry.js +18 -7
  30. package/src/actions/xcodeProject.js +152 -0
  31. package/src/assets/cpp-runtime/node.cpp +18 -1
  32. package/src/assets/js-runtime/adapters/worker-comlink.js +58 -0
  33. package/src/assets/js-runtime/core.js +5 -4
  34. package/src/assets/toolchain-digests.json +25 -19
  35. package/src/assets/wasi-runtime/http/README.md +17 -0
  36. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  37. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  38. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  39. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  40. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  41. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  42. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  43. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  44. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  46. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  47. package/src/bin.js +102 -209
  48. package/src/cli.js +14 -0
  49. package/src/index.js +6 -0
  50. package/src/integration/getCrossbindScript.js +19 -4
  51. package/src/program.js +169 -0
  52. package/src/state/index.js +8 -3
  53. package/src/state/loadConfig.js +6 -11
  54. package/src/utils/appRustCrates.js +7 -1
  55. package/src/utils/bridgeExtras.js +74 -6
  56. package/src/utils/cargoLock.js +19 -0
  57. package/src/utils/cppDts.js +2 -0
  58. package/src/utils/downloadAndExtractFile.js +42 -14
  59. package/src/utils/embindObjectLifetime.js +35 -0
  60. package/src/utils/embindRsFingerprint.js +10 -3
  61. package/src/utils/hermes.js +40 -0
  62. package/src/utils/packageLicense.js +4 -2
  63. package/src/utils/plist.js +42 -0
  64. package/src/utils/pullDockerImage.js +9 -3
  65. package/src/utils/replaceBasePathForDocker.js +8 -0
  66. package/src/utils/resetCMakeCacheForSource.js +26 -0
  67. package/src/utils/resolveAppHost.js +21 -0
  68. package/src/utils/rustBridgeGen.js +13 -4
  69. package/src/utils/rustMt.js +5 -0
  70. package/src/utils/rustSysroot.js +7 -5
  71. package/src/utils/selectRunner.js +1 -1
  72. package/src/utils/swigInterface.js +5 -4
  73. package/src/utils/systemConfig.js +13 -0
  74. package/src/utils/systemKeys.js +4 -0
  75. package/src/utils/targets.js +30 -3
  76. package/src/utils/wasiHttp.js +50 -0
  77. package/vitest.config.js +5 -0
@@ -8,6 +8,11 @@ import familyManifestOf from '../utils/familyManifest.js';
8
8
  import { wasiToolchainIdentity } from '../utils/provenance.js';
9
9
  import { resolveWasiSdkPath } from '../utils/wasiToolchain.js';
10
10
  import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
11
+ import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveAppHost.js';
12
+ import {
13
+ HERMES, HERMES_ANDROID, HERMES_SOURCE, hermesUrl,
14
+ } from '../utils/hermes.js';
15
+ import { ANDROIDX_WEBKIT } from './gradleProject.js';
11
16
  import { shippedBundledLicense } from '../utils/bundledLicenses.js';
12
17
  import toolchainNoticesDir from '../utils/toolchainNotices.js';
13
18
 
@@ -103,7 +108,7 @@ function wasiToolchainRows() {
103
108
  ];
104
109
  }
105
110
 
106
- // The notices a build copied out of its toolchain image (buildNode does it for Windows addons).
111
+ // The notices a build copied out of its toolchain image (a Windows addon's or app's does).
107
112
  function toolchainNoticeTexts(platform) {
108
113
  const build = state.config?.paths?.build;
109
114
  const dir = build ? path.join(build, toolchainNoticesDir(platform)) : null;
@@ -130,33 +135,32 @@ function addonToolchainRows(platform) {
130
135
  };
131
136
  const llvmText = 'license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT';
132
137
  if (platform === 'linux' || platform === 'linuxmusl') {
133
- return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the addon; ${llvmText}` }];
138
+ return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the binary; ${llvmText}` }];
134
139
  }
135
140
  const mingwLicense = 'LicenseRef-MinGW-w64-runtime';
136
141
  return [
137
- { ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the addon; ${llvmText}` },
142
+ { ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the binary; ${llvmText}` },
138
143
  {
139
144
  ...shared,
140
145
  name: 'mingw-w64-runtime',
141
146
  license: mingwLicense,
142
147
  licenseDeclared: mingwLicense,
143
148
  sourceUrl: 'https://github.com/mingw-w64/mingw-w64',
144
- licenseNotes: 'the mingw-w64 startup code and winpthreads, statically linked into the addon; a binary distribution includes their notices: https://github.com/mingw-w64/mingw-w64/blob/master/COPYING.MinGW-w64-runtime/COPYING.MinGW-w64-runtime.txt and https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-libraries/winpthreads/COPYING (the windows image carries both in /opt/licenses/llvm-mingw)',
149
+ licenseNotes: 'the mingw-w64 startup code and winpthreads, statically linked into the binary; a binary distribution includes their notices: https://github.com/mingw-w64/mingw-w64/blob/master/COPYING.MinGW-w64-runtime/COPYING.MinGW-w64-runtime.txt and https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-libraries/winpthreads/COPYING (the windows image carries both in /opt/licenses/llvm-mingw)',
145
150
  licenseText: toolchainNoticeTexts('win32'),
146
151
  },
147
152
  ];
148
153
  }
149
154
 
150
- // A Node-API addon compiles in crossbind's runtime: embind-jsi, which adapts Emscripten's embind,
151
- // and node-api-jsi. The loader bundles the JavaScript half of the same runtime.
152
- function nodeRuntimeRows({ isAddon }) {
153
- const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
154
- const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
155
+ const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
156
+ const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
157
+
158
+ // crossbind's runtime, which an addon and an app host compile in: embind-jsi, which adapts Emscripten's embind.
159
+ function runtimeRows(jsiRoot, notes) {
155
160
  const shared = {
156
161
  nativeVersion: null, sha256: null, licenseSelected: null, isCopyleft: false,
157
162
  };
158
- const jsiRoot = resolveEmbindJsiRoot();
159
- const rows = [
163
+ return [
160
164
  {
161
165
  ...shared,
162
166
  name: 'crossbind',
@@ -165,7 +169,7 @@ function nodeRuntimeRows({ isAddon }) {
165
169
  license: 'MIT',
166
170
  licenseDeclared: 'MIT',
167
171
  sourceUrl: 'https://github.com/crossbind/crossbind',
168
- licenseNotes: 'the embind-jsi and embind-napi runtime and the bindings crossbind generates',
172
+ licenseNotes: notes,
169
173
  licenseText: textOf(path.join(CROSSBIND_ROOT, 'LICENSE')),
170
174
  },
171
175
  {
@@ -180,10 +184,19 @@ function nodeRuntimeRows({ isAddon }) {
180
184
  licenseText: textOf(path.join(jsiRoot, 'LICENSE')),
181
185
  },
182
186
  ];
187
+ }
188
+
189
+ // A Node-API addon compiles in crossbind's runtime and node-api-jsi. The loader bundles the JavaScript half of the
190
+ // same runtime.
191
+ function nodeRuntimeRows({ isAddon }) {
192
+ const rows = runtimeRows(resolveEmbindJsiRoot(), 'the embind-jsi and embind-napi runtime and the bindings crossbind generates');
183
193
  if (!isAddon) return rows;
184
194
  const napiRoot = resolveEmbindNapiRoot();
185
195
  return [...rows, {
186
- ...shared,
196
+ nativeVersion: null,
197
+ sha256: null,
198
+ licenseSelected: null,
199
+ isCopyleft: false,
187
200
  name: 'node-api-jsi',
188
201
  npmName: '@crossbind/core-embind-napi',
189
202
  version: versionOf(napiRoot),
@@ -195,6 +208,137 @@ function nodeRuntimeRows({ isAddon }) {
195
208
  }];
196
209
  }
197
210
 
211
+ // What an app's host carries beside crossbind's runtime, pinned as the third_party READMEs of @crossbind/core-app-host
212
+ // record: React Native's Hermes, JSI's jsi.cpp from the same release, webview/webview, and on Windows the WebView2 SDK's
213
+ // headers.
214
+ const WEBVIEW = { version: 'cbbdee44afff22867de9fd88a9fc8350d9bdd399', sha256: '10e972a2327b5681474f4aa4499e505eaa4ff659aa995380386f08fd6fc1b763' };
215
+ const WEBVIEW2_SDK = { version: '1.0.4258.31', sha256: '56f7f4b8bf9aee4b8efefbbdd4f67d5f74ebd1b100ed0806da71bf76af481aa9' };
216
+ const JSI_SHA256 = 'cb6274a22abc93ecc970066539815e716d3e47716e8f93a9971cf4eb5918ffc1';
217
+ // The fbjni the Hermes release for Android depends on.
218
+ const FBJNI_VERSION = '0.7.0';
219
+
220
+ const HERMES_BUILDS = {
221
+ linux: {
222
+ sha256: HERMES_SOURCE.sha256,
223
+ sourceUrl: HERMES_SOURCE.url,
224
+ licenseNotes: "Hermes built from its release's source in the linux toolchain image, statically linked into the host",
225
+ },
226
+ win32: {
227
+ sha256: HERMES_SOURCE.sha256,
228
+ sourceUrl: HERMES_SOURCE.url,
229
+ licenseNotes: "Hermes built from its release's source in the windows toolchain image (llvm-mingw), statically linked into the host",
230
+ },
231
+ android: {
232
+ sha256: HERMES_ANDROID.sha256,
233
+ sourceUrl: HERMES_ANDROID.url,
234
+ licenseNotes: "React Native's Hermes for Android, whose libraries the app's Gradle build takes from Maven Central",
235
+ },
236
+ };
237
+
238
+ // What an Android app's Gradle build adds from Maven, and the C++ runtime the app carries beside its host.
239
+ function androidAppRows() {
240
+ const shared = {
241
+ npmName: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
242
+ };
243
+ const apache = { ...shared, license: 'Apache-2.0', licenseDeclared: 'Apache-2.0' };
244
+ const llvmLicense = 'Apache-2.0 WITH LLVM-exception';
245
+ return [
246
+ {
247
+ ...apache,
248
+ name: 'fbjni',
249
+ version: FBJNI_VERSION,
250
+ nativeVersion: FBJNI_VERSION,
251
+ sourceUrl: 'https://github.com/facebookincubator/fbjni',
252
+ licenseNotes: 'libfbjni.so, which Hermes loads and Gradle takes with it; license text: https://github.com/facebookincubator/fbjni/blob/main/LICENSE',
253
+ },
254
+ {
255
+ ...apache,
256
+ name: 'androidx-webkit',
257
+ version: ANDROIDX_WEBKIT,
258
+ nativeVersion: ANDROIDX_WEBKIT,
259
+ sourceUrl: 'https://developer.android.com/jetpack/androidx/releases/webkit',
260
+ licenseNotes: "androidx.webkit, the WebView's message ports and start scripts, from Google's Maven repository; license text: https://www.apache.org/licenses/LICENSE-2.0.txt",
261
+ },
262
+ {
263
+ ...shared,
264
+ name: 'llvm-runtimes',
265
+ version: null,
266
+ nativeVersion: null,
267
+ license: llvmLicense,
268
+ licenseDeclared: llvmLicense,
269
+ sourceUrl: 'https://github.com/llvm/llvm-project',
270
+ licenseNotes: "libc++ of the android image's NDK, which the app carries as libc++_shared.so; license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT",
271
+ },
272
+ ];
273
+ }
274
+
275
+ // The WebView2 SDK headers a Windows host compiles against. webview/webview loads the WebView2 Runtime itself, without
276
+ // the SDK's loader.
277
+ function windowsAppRows(hostRoot) {
278
+ const license = 'BSD-3-Clause';
279
+ return [{
280
+ npmName: '@crossbind/core-app-host',
281
+ name: 'webview2-sdk',
282
+ version: WEBVIEW2_SDK.version,
283
+ nativeVersion: WEBVIEW2_SDK.version,
284
+ sha256: WEBVIEW2_SDK.sha256,
285
+ license,
286
+ licenseDeclared: license,
287
+ licenseSelected: null,
288
+ isCopyleft: false,
289
+ sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
290
+ licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
291
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'webview2', 'LICENSE')),
292
+ }];
293
+ }
294
+
295
+ function appRuntimeRows(platform) {
296
+ const hostRoot = resolveAppHostRoot();
297
+ const hermesLicense = textOf(path.join(hostRoot, 'third_party', 'jsi', 'LICENSE'));
298
+ const mit = {
299
+ npmName: '@crossbind/core-app-host', license: 'MIT', licenseDeclared: 'MIT', licenseSelected: null, isCopyleft: false,
300
+ };
301
+ const hermes = HERMES_BUILDS[platform] ?? {
302
+ sha256: HERMES.sha256,
303
+ sourceUrl: hermesUrl(HERMES.version),
304
+ licenseNotes: "React Native's Hermes for Apple platforms, which the app carries as hermesvm.framework",
305
+ };
306
+ return [
307
+ ...runtimeRows(resolveAppEmbindJsiRoot(), 'the embind-jsi runtime, the app host and the bindings crossbind generates'),
308
+ {
309
+ ...mit,
310
+ ...hermes,
311
+ name: 'hermes',
312
+ version: HERMES.version,
313
+ nativeVersion: HERMES.version,
314
+ licenseText: hermesLicense,
315
+ },
316
+ {
317
+ ...mit,
318
+ name: 'jsi',
319
+ version: HERMES.version,
320
+ nativeVersion: HERMES.version,
321
+ sha256: JSI_SHA256,
322
+ sourceUrl: 'https://github.com/facebook/hermes',
323
+ licenseNotes: "JSI's jsi.cpp from the same Hermes release, compiled into the host",
324
+ licenseText: hermesLicense,
325
+ },
326
+ ...(platform === 'android' ? androidAppRows() : []),
327
+ ...(platform === 'win32' ? windowsAppRows(hostRoot) : []),
328
+ // A mobile app makes its window with UIKit or an Android activity, without webview/webview.
329
+ ...(['ios', 'android'].includes(platform) ? [] : [{
330
+ ...mit,
331
+ name: 'webview',
332
+ version: WEBVIEW.version,
333
+ nativeVersion: WEBVIEW.version,
334
+ sha256: WEBVIEW.sha256,
335
+ sourceUrl: 'https://github.com/webview/webview',
336
+ licenseNotes: 'webview/webview, with the local patch @crossbind/core-app-host lists',
337
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
338
+ }]),
339
+ ];
340
+ }
341
+
198
342
  async function buildRow(node) {
199
343
  const recipe = await loadJs(node.paths.project, 'crossbind.build');
200
344
  const manifest = node.package || null;
@@ -284,7 +428,8 @@ function assertConanStaged() {
284
428
  // platform (optional, e.g. 'wasi') additionally includes what that platform's
285
429
  // artifact statically links beyond the package graph: recipe-declared vendored
286
430
  // copies and the toolchain runtime. runtimeEnv 'node' adds the crossbind runtime an
287
- // addon of that platform compiles in, or without a platform the one its loader bundles.
431
+ // addon of that platform compiles in, or without a platform the one its loader bundles;
432
+ // runtimeEnv 'app' adds what an app's host carries.
288
433
  export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
289
434
  assertConanStaged();
290
435
  // The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
@@ -310,5 +455,6 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
310
455
  if (platform === 'wasi') rows.push(...wasiToolchainRows());
311
456
  if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
312
457
  if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
458
+ if (runtimeEnv === 'app') rows.push(...appRuntimeRows(platform));
313
459
  return rows.sort((a, b) => a.name.localeCompare(b.name));
314
460
  }
@@ -0,0 +1,35 @@
1
+ import createBridgeFile from './createInterface.js';
2
+ import { createImportedBridges, findAppNativeImports } from './createImportedBridges.js';
3
+ import getDependFilePath from '../integration/getDependFilePath.js';
4
+ import state from '../state/index.js';
5
+ import findFiles from '../utils/findFiles.js';
6
+ import {
7
+ boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders,
8
+ } from '../utils/boundHeaders.js';
9
+
10
+ // The project's own headers, each with its bridge.
11
+ export function createBridges(target) {
12
+ let headers = [];
13
+ state.config.paths.header.forEach((headerPath) => {
14
+ headers.push(findFiles('**/*.h', { cwd: headerPath }));
15
+ });
16
+ headers = headers.filter((path) => !!path.toString()).flat();
17
+
18
+ return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
19
+ }
20
+
21
+ // The dependency headers the config binds whole, each read where one of the targets' packages ships it.
22
+ export function createBoundBridges(targets) {
23
+ const headers = resolveBoundHeaders(boundHeaderSpecifiers(state.config), bridgeTargets(targets, state.targets), getDependFilePath);
24
+ const wholeHeaders = headers.map((header) => header.file);
25
+ return headers.map((header) => ({ ...header, bridge: createBridgeFile(header.file, header.target, { wholeHeaders }) }));
26
+ }
27
+
28
+ // What a native binary binds, an addon or an app host: the project's headers, the dependency headers the config binds
29
+ // whole, and the ones the app's sources import.
30
+ export function createNativeBridges(targets) {
31
+ const boundHeaders = createBoundBridges(targets);
32
+ const ownHeaders = [...createBridges(nodeBridgeTarget(targets)), ...boundHeaders];
33
+ const imports = findAppNativeImports(bridgeTargets(targets, state.targets));
34
+ return { headers: [...ownHeaders, ...createImportedBridges(ownHeaders, imports)], imports };
35
+ }
@@ -22,7 +22,7 @@ import {
22
22
  } from '../utils/darwinToolchain.js';
23
23
 
24
24
  // Native builds can outrun Node's 1 MiB default pipe buffer; without a raised cap a successful build dies with ENOBUFS.
25
- const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
25
+ export const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
26
26
  // Containers share the memory of Docker's VM, and one compile at full -j can take most of a desktop VM
27
27
  // (GDAL's bindings peak at 7.5 of 7.7 GiB), so the exclusive steps of parallel builds take turns.
28
28
  const DOCKER_COMPILE_LOCK = path.join(os.homedir(), '.crossbind', 'docker-compile.lock');
@@ -124,7 +124,7 @@ const iosParams = [
124
124
 
125
125
  // The C++ compiler of the image that runs SWIG for the target, so its -dM macro table matches that parse.
126
126
  export function cxxPreprocessorFor(target) {
127
- if (imageRoleFor(target) !== 'android') return 'em++';
127
+ if (target?.platform !== 'android') return 'em++';
128
128
  return `${t}/${target.arch === 'x86_64' ? CROSSCOMPILER_x86_64 : CROSSCOMPILER_ARM64}-clang++`;
129
129
  }
130
130
 
@@ -196,7 +196,7 @@ export default function run(program, params = [], platformPrefix = null, target
196
196
  case 'android':
197
197
  [dProgram, ...dParams] = params;
198
198
  platformParams = target.arch === 'x86_64' ? androidParamsX86_64 : androidParamsArm64;
199
- if (dProgram === 'cmake') {
199
+ if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
200
200
  dParams = [
201
201
  ...dParams,
202
202
  `-DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK}/build/cmake/android.toolchain.cmake`,
@@ -40,6 +40,7 @@ const BOOT_SCRIPT = [
40
40
  const VERSION = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
41
41
 
42
42
  const runnerName = (role) => `crossbind-runner-${role}`;
43
+ const localRunnerName = (role) => `crossbind-local-runner-${role}`;
43
44
  const runnerFiles = () => fs.readdirSync(RUNNER_SOURCE).filter((file) => file.endsWith('.js'));
44
45
  // Google ships the linux NDK for x86_64 only, so an android runner is always amd64.
45
46
  const platformOf = (role) => (role === 'android' ? 'linux/amd64' : undefined);
@@ -62,7 +63,7 @@ function assertRole(role) {
62
63
  export function runnerStartArgs({ role, port, host }) {
63
64
  const platform = platformOf(role);
64
65
  return [
65
- 'run', '-d', '--name', runnerName(role), '--restart', 'unless-stopped',
66
+ 'run', '-d', '--name', localRunnerName(role), '--restart', 'unless-stopped',
66
67
  ...(platform ? ['--platform', platform] : []),
67
68
  ...DOCKER_RUN_SECURITY_ARGS,
68
69
  '-p', `${host}:${port}:${RUNNER_PORT}`,
@@ -89,13 +90,24 @@ export function startRunner({ role = 'web', port = runnerHostPort(role), host =
89
90
  });
90
91
  // A token taken from the environment is shown by its variable, so the line printed for it puts no secret in a log.
91
92
  return {
92
- name: runnerName(role), url: `http://${host}:${port}`, token: secret, displayToken: fromEnvironment ? '$CROSSBIND_RUNNER_TOKEN' : secret,
93
+ name: localRunnerName(role), url: `http://${host}:${port}`, token: secret, displayToken: fromEnvironment ? '$CROSSBIND_RUNNER_TOKEN' : secret,
93
94
  };
94
95
  }
95
96
 
96
- export function stopRunner({ role = 'web' } = {}) {
97
+ // A container that is not there is already deleted: docker reports one on stderr even with -f, and some engines exit 1.
98
+ function removeContainer(name) {
99
+ try {
100
+ execFileSync('docker', ['rm', '-f', name], { stdio: ['ignore', 'inherit', 'pipe'] });
101
+ } catch (error) {
102
+ if (!/no such container/i.test(String(error.stderr))) throw error;
103
+ }
104
+ }
105
+
106
+ export function deleteRunner({ role = 'web' } = {}) {
97
107
  assertRole(role);
98
- execFileSync('docker', ['rm', '-f', runnerName(role)], { stdio: 'inherit' });
108
+ removeContainer(localRunnerName(role));
109
+ // An older crossbind started its local runner under the name the deployed runners take.
110
+ removeContainer(runnerName(role));
99
111
  }
100
112
 
101
113
  function dockerfile(role) {
@@ -28,7 +28,8 @@ export function getTargetParams(givenTargetParams = {}, noParamCheck = false) {
28
28
  }
29
29
 
30
30
  export function getBuildTargets(targetParams) {
31
- const { platform, arch, runtime, buildType, runtimeEnv } = targetParams;
31
+ // A lookup that names no runtime environment finds the targets that have none: the archives' alone.
32
+ const { platform, arch, runtime, buildType, runtimeEnv = [] } = targetParams;
32
33
  return state.targets.filter(t => (
33
34
  (!t.platform || platform.includes(t.platform))
34
35
  && (!t.arch || arch.includes(t.arch))
@@ -36,13 +36,10 @@ export default function writeRuntimeEntry(entry, headers, runtime) {
36
36
 
37
37
  const IMPORT_HOOKS = new URL('../assets/node-import-hooks.mjs', import.meta.url);
38
38
 
39
- // The module `node --import` loads ahead of the app, so the app imports the bound headers themselves: each
40
- // header by its real path, with the bare specifiers that name it (imports, from findAppNativeImports) and its
41
- // names, each with the entry's name for it where the two differ.
42
- export function writeImportHooks(entry, headers, imports, extensions) {
43
- // Node runs the register file from its real path, so the files it names are relative to that.
44
- const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
45
- const natives = headers.map((header) => {
39
+ // Each bound header and Rust import by its real path relative to dir, with the bare specifiers that name it (imports,
40
+ // from findAppNativeImports) and its names, each with the module's name for it where the two differ.
41
+ function nativesOf(headers, imports, dir) {
42
+ return headers.map((header) => {
46
43
  const file = upath.normalize(fs.realpathSync(header.file));
47
44
  return {
48
45
  file: upath.relative(dir, file),
@@ -50,6 +47,13 @@ export function writeImportHooks(entry, headers, imports, extensions) {
50
47
  names: uniqueBindings(namesOf(header)).map(({ local, wire }) => (local === wire ? local : { local, wire })),
51
48
  };
52
49
  });
50
+ }
51
+
52
+ // The module `node --import` loads ahead of the app, so the app imports the bound headers themselves.
53
+ export function writeImportHooks(entry, headers, imports, extensions) {
54
+ // Node runs the register file from its real path, so the files it names are relative to that.
55
+ const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
56
+ const natives = nativesOf(headers, imports, dir);
53
57
  writeIfChanged(`${dir}/hooks.mjs`, fs.readFileSync(IMPORT_HOOKS, 'utf8'));
54
58
  writeIfChanged(entry.replace(/\.mjs$/, '.register.mjs'), [
55
59
  '// Generated by crossbind - do not edit.',
@@ -60,6 +64,13 @@ export function writeImportHooks(entry, headers, imports, extensions) {
60
64
  ].join('\n'));
61
65
  }
62
66
 
67
+ // Beside a browser binary built with --bind-imports: what a page that resolves the app's imports itself (the
68
+ // playground) maps them with, each file relative to the project.
69
+ export function writeImportManifest(file, headers, imports, projectDir) {
70
+ const natives = nativesOf(headers, imports, upath.normalize(fs.realpathSync(projectDir)));
71
+ writeIfChanged(file, `${JSON.stringify({ natives }, null, 4)}\n`);
72
+ }
73
+
63
74
  // Once the sources import nothing native, no start loads hooks an earlier build left; hooks.mjs stays while the other
64
75
  // binary's hooks still import it.
65
76
  export function removeImportHooks(entry) {
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import state from '../state/index.js';
4
+ import { IOS_DEPLOYMENT_TARGET } from '../utils/iosToolchain.js';
5
+ import { textPlist, xmlPlist } from '../utils/plist.js';
6
+
7
+ // The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
8
+ export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
9
+ export const HERMES_SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64_x86_64-simulator' };
10
+ // What the host's own code needs from the system: Xcode adds it only for the sources it compiles itself.
11
+ const SYSTEM_LIBS = ['-framework', 'UIKit', '-framework', 'WebKit', '-framework', 'CoreGraphics', '-lc++'];
12
+
13
+ const MAIN = `// crossbind writes this project anew on every build: the app comes prebuilt in lib/, and its files are in app/.
14
+ int crossbind_app_main(int argc, char *argv[]);
15
+
16
+ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
17
+ `;
18
+
19
+ const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
20
+ 'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
21
+ 'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs'];
22
+ // Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
23
+ const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
24
+
25
+ const PROJECT_SETTINGS = {
26
+ SDKROOT: 'iphoneos',
27
+ SUPPORTED_PLATFORMS: 'iphoneos iphonesimulator',
28
+ IPHONEOS_DEPLOYMENT_TARGET: IOS_DEPLOYMENT_TARGET,
29
+ TARGETED_DEVICE_FAMILY: '1,2',
30
+ // crossbind's iOS archives are arm64 alone, for devices and the Simulator alike.
31
+ ARCHS: 'arm64',
32
+ ALWAYS_SEARCH_USER_PATHS: 'NO',
33
+ CLANG_ENABLE_OBJC_ARC: 'YES',
34
+ DEAD_CODE_STRIPPING: 'YES',
35
+ };
36
+
37
+ const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
38
+
39
+ const infoPlist = (name) => xmlPlist({
40
+ CFBundleDevelopmentRegion: 'en',
41
+ CFBundleDisplayName: name,
42
+ CFBundleExecutable: '$(EXECUTABLE_NAME)',
43
+ CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
44
+ CFBundleInfoDictionaryVersion: '6.0',
45
+ CFBundleName: '$(PRODUCT_NAME)',
46
+ CFBundlePackageType: 'APPL',
47
+ CFBundleShortVersionString: '$(MARKETING_VERSION)',
48
+ CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
49
+ LSRequiresIPhoneOS: true,
50
+ UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
51
+ UILaunchScreen: {},
52
+ UISupportedInterfaceOrientations: ORIENTATIONS,
53
+ 'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
54
+ });
55
+
56
+ function objects(name, targetSettings) {
57
+ const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
58
+ const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
59
+ const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
60
+ const configurations = (debug, release) => ({
61
+ isa: 'XCConfigurationList', buildConfigurations: [debug, release], defaultConfigurationIsVisible: 0, defaultConfigurationName: 'Release',
62
+ });
63
+ return {
64
+ [ID.main]: file('main.m', 'sourcecode.c.objc'),
65
+ [ID.info]: file('Info.plist', 'text.plist.xml'),
66
+ [ID.app]: file('app', 'folder'),
67
+ [ID.hermes]: file('Frameworks/hermesvm.xcframework', 'wrapper.xcframework'),
68
+ [ID.product]: {
69
+ isa: 'PBXFileReference', explicitFileType: 'wrapper.application', includeInIndex: 0, path: `${name}.app`, sourceTree: 'BUILT_PRODUCTS_DIR',
70
+ },
71
+ [ID.mainBuild]: { isa: 'PBXBuildFile', fileRef: ID.main },
72
+ [ID.appBuild]: { isa: 'PBXBuildFile', fileRef: ID.app },
73
+ [ID.hermesLink]: { isa: 'PBXBuildFile', fileRef: ID.hermes },
74
+ [ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
75
+ [ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
76
+ [ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
77
+ [ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild]),
78
+ [ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
79
+ [ID.mainGroup]: { isa: 'PBXGroup', children: [ID.main, ID.info, ID.app, ID.hermes, ID.products], sourceTree: '<group>' },
80
+ [ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
81
+ [ID.target]: {
82
+ isa: 'PBXNativeTarget',
83
+ buildConfigurationList: ID.targetConfigs,
84
+ buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed],
85
+ buildRules: [],
86
+ dependencies: [],
87
+ name,
88
+ productName: name,
89
+ productReference: ID.product,
90
+ productType: 'com.apple.product-type.application',
91
+ },
92
+ [ID.project]: {
93
+ isa: 'PBXProject',
94
+ buildConfigurationList: ID.projectConfigs,
95
+ compatibilityVersion: 'Xcode 14.0',
96
+ developmentRegion: 'en',
97
+ hasScannedForEncodings: 0,
98
+ knownRegions: ['en', 'Base'],
99
+ mainGroup: ID.mainGroup,
100
+ productRefGroup: ID.products,
101
+ projectDirPath: '',
102
+ projectRoot: '',
103
+ targets: [ID.target],
104
+ },
105
+ [ID.projectDebug]: configuration('Debug', PROJECT_SETTINGS),
106
+ [ID.projectRelease]: configuration('Release', PROJECT_SETTINGS),
107
+ [ID.targetDebug]: configuration('Debug', targetSettings),
108
+ [ID.targetRelease]: configuration('Release', targetSettings),
109
+ [ID.projectConfigs]: configurations(ID.projectDebug, ID.projectRelease),
110
+ [ID.targetConfigs]: configurations(ID.targetDebug, ID.targetRelease),
111
+ };
112
+ }
113
+
114
+ // An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
115
+ // their link line, and its slice of Hermes; the first also the app's files. Xcode signs the app and runs it, in the
116
+ // Simulator or on a device.
117
+ export default function writeXcodeProject(to, { parts, identity, version }) {
118
+ const { name } = state.config.general;
119
+ if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
120
+ fs.rmSync(to, { recursive: true, force: true });
121
+ fs.mkdirSync(`${to}/${name}.xcodeproj`, { recursive: true });
122
+ const linkFlags = {};
123
+ parts.forEach(({ sdk, dir }) => {
124
+ fs.cpSync(`${dir}/lib`, `${to}/lib/${sdk}`, { recursive: true });
125
+ // The quotes keep a project path with spaces in one argument.
126
+ const args = JSON.parse(fs.readFileSync(`${dir}/link.json`, 'utf8'))
127
+ .map((arg) => arg.replace(/\{lib\}\/(.+)$/, `"$(PROJECT_DIR)/lib/${sdk}/$1"`));
128
+ linkFlags[`OTHER_LDFLAGS[sdk=${sdk}*]`] = [...args, ...SYSTEM_LIBS];
129
+ });
130
+ fs.cpSync(`${parts[0].dir}/app`, `${to}/app`, { recursive: true });
131
+ const frameworks = parts.flatMap(({ dir }) => ['-framework', `${dir}/hermesvm.framework`]);
132
+ const result = spawnSync('xcodebuild', ['-create-xcframework', ...frameworks, '-output', `${to}/Frameworks/hermesvm.xcframework`], { encoding: 'utf8' });
133
+ if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
134
+
135
+ fs.writeFileSync(`${to}/main.m`, MAIN);
136
+ fs.writeFileSync(`${to}/Info.plist`, infoPlist(name));
137
+ const targetSettings = {
138
+ PRODUCT_NAME: name,
139
+ PRODUCT_BUNDLE_IDENTIFIER: identity.id,
140
+ DEVELOPMENT_TEAM: identity.team,
141
+ CODE_SIGN_STYLE: 'Automatic',
142
+ INFOPLIST_FILE: 'Info.plist',
143
+ MARKETING_VERSION: version,
144
+ CURRENT_PROJECT_VERSION: version,
145
+ LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
146
+ ...linkFlags,
147
+ };
148
+ fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
149
+ archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings), rootObject: ID.project,
150
+ }));
151
+ return to;
152
+ }
@@ -2,9 +2,26 @@
2
2
  #include <emscripten/wasmfs.h>
3
3
  #include <limits.h>
4
4
  #include <unistd.h>
5
+ #include <wasi/api.h>
6
+
7
+ static backend_t hostBackend;
5
8
 
6
9
  backend_t wasmfs_create_root_dir(void) {
7
- return wasmfs_create_node_backend("/");
10
+ hostBackend = wasmfs_create_node_backend("/");
11
+ return hostBackend;
12
+ }
13
+
14
+ // Emscripten's Node backend traps on flush, which fsync and fdatasync call. A host file's writes have already
15
+ // reached the operating system, so both succeed there, as with NODERAWFS. buildWasm links these with --wrap.
16
+ extern "C" __wasi_errno_t __real___wasi_fd_sync(__wasi_fd_t fd);
17
+ extern "C" int __real___syscall_fdatasync(int fd);
18
+
19
+ extern "C" __wasi_errno_t __wrap___wasi_fd_sync(__wasi_fd_t fd) {
20
+ return wasmfs_get_backend_by_fd(fd) == hostBackend ? __WASI_ERRNO_SUCCESS : __real___wasi_fd_sync(fd);
21
+ }
22
+
23
+ extern "C" int __wrap___syscall_fdatasync(int fd) {
24
+ return wasmfs_get_backend_by_fd(fd) == hostBackend ? 0 : __real___syscall_fdatasync(fd);
8
25
  }
9
26
 
10
27
  void wasmfs_before_preload(void) {