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
package/AGENTS.md CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  ## What lives here
6
6
 
7
- - `src/bin.js` — `crossbind` CLI entry; commander commands, target filtering and top-level orchestration.
7
+ - `src/cli.js` — `crossbind` CLI entry: `login`, `logout`, `usage`, `account`, `config` and `--version` run from `src/program.js` without loading a project (only `config list -t all|project` loads it); every other command goes to `src/bin.js`.
8
+ - `src/bin.js` — the commands that need a project; commander commands, target filtering and top-level orchestration.
8
9
  - `src/index.js` — public API barrel re-exported as the `crossbind` npm package (`createLib`, `buildWasm`, `state`, `getData`, `isSourceNewer`, …).
9
10
  - `src/actions/` — the actual build steps. **Most edits land here.**
10
11
  - `src/state/` — config loading + target matrix.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.63",
3
+ "version": "2.0.0-beta.65",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": "https://github.com/crossbind/crossbind.git",
@@ -10,7 +10,7 @@
10
10
  "node": ">=24"
11
11
  },
12
12
  "bin": {
13
- "crossbind": "./src/bin.js"
13
+ "crossbind": "./src/cli.js"
14
14
  },
15
15
  "main": "src/index.js",
16
16
  "exports": {
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { ANDROID_ABIS } from './gradleProject.js';
6
+ import { EXEC_MAX_BUFFER } from './run.js';
7
+
8
+ const ACTIVITY = 'dev.crossbind.app.CrossbindActivity';
9
+
10
+ // The adb that reaches the device: the one on PATH, else the platform tools of ANDROID_HOME's SDK or Android Studio's.
11
+ // crossbind installs none.
12
+ function adbPath() {
13
+ const sdk = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
14
+ || path.join(os.homedir(), process.platform === 'darwin' ? 'Library/Android/sdk' : 'Android/Sdk');
15
+ const found = [...(process.env.PATH ?? '').split(path.delimiter).map((dir) => path.join(dir, 'adb')), path.join(sdk, 'platform-tools', 'adb')]
16
+ .find((file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile());
17
+ if (!found) {
18
+ throw new Error('crossbind: an Android app runs on a device through adb, which is not on PATH nor in an Android SDK. Install Android\'s platform tools: `brew install --cask android-platform-tools` on macOS, `sudo apt install adb` on Debian and Ubuntu, or Android Studio.');
19
+ }
20
+ return found;
21
+ }
22
+
23
+ function tool(program, args, options = {}) {
24
+ const result = spawnSync(program, args, { encoding: 'utf8', maxBuffer: EXEC_MAX_BUFFER, ...options });
25
+ if (result.status !== 0) {
26
+ throw new Error(`crossbind: ${path.basename(program)} ${args.join(' ')} failed: ${`${result.stdout ?? ''}${result.stderr ?? ''}`.trim() || result.error?.message}`);
27
+ }
28
+ return result.stdout;
29
+ }
30
+
31
+ const adb = (args) => tool(adbPath(), args);
32
+ // adb shell hands the device's shell one line, which splits it again.
33
+ const quoted = (text) => `'${String(text).replace(/'/g, "'\\''")}'`;
34
+
35
+ // The device an app runs on in development and in tests: the one ANDROID_SERIAL names, else the only one attached, and
36
+ // the ABI of it the app builds for. crossbind starts no emulator.
37
+ export function attachedDevice() {
38
+ const serials = adb(['devices']).split('\n').slice(1).map((line) => line.trim().split('\t'))
39
+ .filter(([, status]) => status === 'device').map(([serial]) => serial);
40
+ const serial = process.env.ANDROID_SERIAL || (serials.length === 1 ? serials[0] : null);
41
+ if (!serial) {
42
+ throw new Error(serials.length === 0
43
+ ? 'crossbind: no Android device is attached. Start an emulator, in Android Studio\'s Device Manager or with `emulator -avd <name>`, or connect a phone with USB debugging on.'
44
+ : `crossbind: ${serials.length} Android devices are attached (${serials.join(', ')}). Set ANDROID_SERIAL to the one the app runs on.`);
45
+ }
46
+ const abis = adb(['-s', serial, 'shell', 'getprop', 'ro.product.cpu.abilist']).trim().split(',');
47
+ const abi = abis.find((one) => ANDROID_ABIS.includes(one));
48
+ if (!abi) throw new Error(`crossbind: the Android device ${serial} runs ${abis.join(', ')}, and apps build for ${ANDROID_ABIS.join(' and ')}.`);
49
+ return { serial, abi };
50
+ }
51
+
52
+ // Installs an APK on the attached device and opens it, with `env` as the activity's extras; a dev server's page on this
53
+ // machine reaches the device through adb reverse. The process returned carries the app's log, its page's console among
54
+ // it, and ends with the app; close() ends the app.
55
+ export default function openOnDevice(apk, { env = {}, stdio = 'pipe' } = {}) {
56
+ const { serial } = attachedDevice();
57
+ const id = JSON.parse(fs.readFileSync(path.join(path.dirname(apk), 'output-metadata.json'), 'utf8')).applicationId;
58
+ try {
59
+ adb(['-s', serial, 'install', '-r', '-d', apk]);
60
+ } catch (error) {
61
+ if (!error.message.includes('INSTALL_FAILED_UPDATE_INCOMPATIBLE')) throw error;
62
+ // As the one Google Play installs: only an uninstall replaces it, which takes its data, so the developer runs it.
63
+ throw new Error(`crossbind: ${id} on ${serial} is signed with another key than this build, so it cannot be updated; \`adb -s ${serial} uninstall ${id}\` removes it, with its data, and the next run installs this one.`, { cause: error });
64
+ }
65
+ const devPort = env.CROSSBIND_APP_URL && new URL(env.CROSSBIND_APP_URL).port;
66
+ if (devPort) adb(['-s', serial, 'reverse', `tcp:${devPort}`, `tcp:${devPort}`]);
67
+ const extras = Object.entries(env).flatMap(([key, value]) => ['--es', key, quoted(value)]);
68
+ adb(['-s', serial, 'shell', 'am', 'start', '-S', '-W', '-n', `${id}/${ACTIVITY}`, ...extras]);
69
+ const pid = spawnSync(adbPath(), ['-s', serial, 'shell', 'pidof', id], { encoding: 'utf8' }).stdout?.trim();
70
+ if (!pid) throw new Error(`crossbind: ${id} stopped as it started on ${serial}; its log is in \`adb logcat\`.`);
71
+ // The log of the app's process alone, without the divider logcat starts with, until the process ends. The pattern
72
+ // that ends logcat names it first, so it matches not the shell whose command line holds it too.
73
+ const logcat = `logcat -v raw --pid=${pid} -s Crossbind:V`;
74
+ const child = spawn(adbPath(), ['-s', serial, 'shell',
75
+ `${logcat} | grep -v '^--------- ' & while [ -d /proc/${pid} ]; do sleep 1; done; pkill -f '^${logcat}'`], { stdio });
76
+ return { child, close: () => spawnSync(adbPath(), ['-s', serial, 'shell', 'am', 'force-stop', id]) };
77
+ }
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import state from '../state/index.js';
6
+ import getOsUserAndGroupId from '../utils/getOsUserAndGroupId.js';
7
+ import pullDockerImage, { getDockerImage } from '../utils/pullDockerImage.js';
8
+ import { DOCKER_BASE, isUnderBase } from '../utils/replaceBasePathForDocker.js';
9
+ import { DOCKER_RUN_SECURITY_ARGS } from '../utils/dockerSecurity.js';
10
+ import { getContentHash } from '../utils/hash.js';
11
+
12
+ const ROLE = 'android-app';
13
+ const PLATFORM = 'linux/amd64';
14
+ const KEYSTORE = '/tmp/crossbind/keystore';
15
+ const GRADLE = ['gradle', '--offline', '--no-watch-fs', '--console=plain', '-q'];
16
+ const OUTPUT_LIMIT = 64 * 1024 * 1024;
17
+ const FAILURE_LINES = 40;
18
+
19
+ // The upload key a release is signed with, named by the environment alone so that no secret reaches a file.
20
+ export const SIGNING = {
21
+ keystore: 'CROSSBIND_ANDROID_KEYSTORE',
22
+ alias: 'CROSSBIND_ANDROID_KEY_ALIAS',
23
+ password: 'CROSSBIND_ANDROID_KEYSTORE_PASSWORD',
24
+ keyPassword: 'CROSSBIND_ANDROID_KEY_PASSWORD',
25
+ };
26
+
27
+ const listed = (names) => `${names.join(' and ')} ${names.length > 1 ? 'are' : 'is'}`;
28
+
29
+ // The keystore of the upload key, or null for none: a release is then signed with the debug key.
30
+ export function releaseSigning(env = process.env) {
31
+ if (!env[SIGNING.keystore]) {
32
+ const stray = [SIGNING.alias, SIGNING.password, SIGNING.keyPassword].filter((name) => env[name]);
33
+ if (stray.length) throw new Error(`crossbind: ${listed(stray)} set without ${SIGNING.keystore}, the keystore of the upload key.`);
34
+ return null;
35
+ }
36
+ const missing = [SIGNING.alias, SIGNING.password].filter((name) => !env[name]);
37
+ if (missing.length) throw new Error(`crossbind: ${SIGNING.keystore} is set, but ${listed(missing)} not.`);
38
+ const file = path.resolve(env[SIGNING.keystore]);
39
+ if (!fs.statSync(file, { throwIfNoEntry: false })?.isFile()) throw new Error(`crossbind: ${SIGNING.keystore} names ${file}, which is no file.`);
40
+ return { file, names: [SIGNING.alias, SIGNING.password, SIGNING.keyPassword].filter((name) => env[name]) };
41
+ }
42
+
43
+ // Gradle's caches and the Android tools' home outlive a container; the second keeps the debug key, which a new one would
44
+ // replace, so that a later build still installs over the last.
45
+ function homes() {
46
+ const root = path.join(os.homedir(), '.crossbind', 'android');
47
+ return [[path.join(root, 'gradle'), '/var/cache/crossbind/gradle'], [path.join(root, 'home'), '/var/cache/crossbind/android']];
48
+ }
49
+
50
+ // A container of crossbind's android-app image, with no network: the project under the build's base, the homes, and
51
+ // the keystore read-only, its passwords passed by name from this environment.
52
+ function containerArgs(signing) {
53
+ return [
54
+ ...DOCKER_RUN_SECURITY_ARGS, '--network', 'none', '--platform', PLATFORM, '--user', getOsUserAndGroupId(),
55
+ '-v', `${state.config.paths.base}:${DOCKER_BASE}`,
56
+ ...homes().flatMap(([host, container]) => {
57
+ fs.mkdirSync(host, { recursive: true });
58
+ return ['-v', `${host}:${container}`];
59
+ }),
60
+ ...(signing ? ['-v', `${signing.file}:${KEYSTORE}:ro`, '-e', `${SIGNING.keystore}=${KEYSTORE}`, ...signing.names.flatMap((name) => ['-e', name])] : []),
61
+ ];
62
+ }
63
+
64
+ function docker(args, what) {
65
+ const result = spawnSync('docker', args, { encoding: 'utf8', maxBuffer: OUTPUT_LIMIT });
66
+ if (result.error) throw new Error(`crossbind: cannot run docker for ${what}: ${result.error.message}`, { cause: result.error });
67
+ if (result.status !== 0) {
68
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().split('\n').slice(-FAILURE_LINES).join('\n');
69
+ throw new Error(`crossbind: ${what} failed in the android-app image with exit code ${result.status}:\n${output}`);
70
+ }
71
+ }
72
+
73
+ // The container sees the project's base alone, under its own POSIX path whatever the host's separators.
74
+ function inContainer(project) {
75
+ const dir = path.resolve(project);
76
+ if (!isUnderBase(dir, state.config.paths.base)) {
77
+ throw new Error(`crossbind: the Android project ${dir} is outside ${state.config.paths.base}, the project's base and all the android-app container sees. Build the app into a directory under it.`);
78
+ }
79
+ return path.posix.join(DOCKER_BASE, ...path.relative(state.config.paths.base, dir).split(path.sep));
80
+ }
81
+
82
+ // An Android app as the files it ships: its release APK, and with an upload key also the bundle Google Play takes, beside
83
+ // the Gradle project as <project>.apk and <project>.aab.
84
+ export function packageAndroidRelease(project) {
85
+ const signing = releaseSigning();
86
+ pullDockerImage(ROLE, PLATFORM);
87
+ docker(['run', '--rm', ...containerArgs(signing), '-w', inContainer(project), getDockerImage(ROLE, PLATFORM),
88
+ ...GRADLE, '--no-daemon', 'assembleRelease', ...(signing ? ['bundleRelease'] : [])], `packaging ${path.basename(project)}`);
89
+ const outputs = `${project}/app/build/outputs`;
90
+ fs.copyFileSync(`${outputs}/apk/release/app-release.apk`, `${project}.apk`);
91
+ fs.rmSync(`${project}.aab`, { force: true });
92
+ if (signing) {
93
+ fs.copyFileSync(`${outputs}/bundle/release/app-release.aab`, `${project}.aab`);
94
+ } else {
95
+ console.warn(`crossbind: ${path.basename(project)}.apk is signed with this machine's debug key: it installs for testing, and Google Play takes no app signed so. Set ${SIGNING.keystore}, ${SIGNING.alias} and ${SIGNING.password} to sign it with your upload key and make the bundle too.`);
96
+ }
97
+ }
98
+
99
+ // During development one container per project, whose Gradle daemon stays warm from one build to the next; it goes with
100
+ // the process.
101
+ let devContainer = null;
102
+
103
+ function startDevContainer() {
104
+ const name = `crossbind-android-app-${getContentHash(state.config.paths.project).slice(0, 12)}`;
105
+ pullDockerImage(ROLE, PLATFORM);
106
+ // One a crashed session left behind holds the name.
107
+ spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
108
+ docker(['run', '--detach', '--rm', '--init', '--name', name, ...containerArgs(null), getDockerImage(ROLE, PLATFORM), 'sleep', 'infinity'],
109
+ 'starting the development container');
110
+ process.once('exit', () => spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' }));
111
+ // A closed terminal ends the session through its exit hooks, as Ctrl-C does.
112
+ process.once('SIGHUP', () => process.exit(129));
113
+ return name;
114
+ }
115
+
116
+ // Builds an Android app's Gradle project as a debug APK, signed with the debug key the homes keep, and returns the APK.
117
+ export function buildApk(project) {
118
+ devContainer ??= startDevContainer();
119
+ docker(['exec', '-w', inContainer(project), devContainer, ...GRADLE, 'assembleDebug'], `building ${path.basename(project)}`);
120
+ return path.join(project, 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
121
+ }