crossbind 2.0.0-beta.63 → 2.0.0-beta.64

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 (50) hide show
  1. package/AGENTS.md +2 -1
  2. package/package.json +2 -2
  3. package/src/actions/buildBinTools.js +0 -0
  4. package/src/actions/buildJs.js +2 -9
  5. package/src/actions/buildNode.js +4 -6
  6. package/src/actions/buildWasiCommand.js +10 -5
  7. package/src/actions/buildWasm.js +24 -3
  8. package/src/actions/createImportedBridges.js +7 -0
  9. package/src/actions/createInterface.js +3 -3
  10. package/src/actions/createLib.js +14 -2
  11. package/src/actions/getData.js +2 -1
  12. package/src/actions/getEnv.js +7 -0
  13. package/src/actions/runnerCommands.js +16 -4
  14. package/src/actions/writeRuntimeEntry.js +18 -7
  15. package/src/assets/cpp-runtime/node.cpp +18 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +51 -0
  17. package/src/assets/js-runtime/core.js +5 -4
  18. package/src/assets/toolchain-digests.json +19 -19
  19. package/src/assets/wasi-runtime/http/README.md +17 -0
  20. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  21. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  22. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  23. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  24. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  25. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  26. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  27. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  28. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  29. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  30. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  31. package/src/bin.js +29 -179
  32. package/src/cli.js +14 -0
  33. package/src/integration/getCrossbindScript.js +4 -3
  34. package/src/program.js +169 -0
  35. package/src/state/index.js +4 -3
  36. package/src/state/loadConfig.js +4 -11
  37. package/src/utils/appRustCrates.js +4 -0
  38. package/src/utils/bridgeExtras.js +74 -6
  39. package/src/utils/cargoLock.js +19 -0
  40. package/src/utils/cppDts.js +2 -0
  41. package/src/utils/embindObjectLifetime.js +35 -0
  42. package/src/utils/packageLicense.js +4 -2
  43. package/src/utils/resetCMakeCacheForSource.js +26 -0
  44. package/src/utils/rustBridgeGen.js +13 -4
  45. package/src/utils/rustMt.js +5 -0
  46. package/src/utils/rustSysroot.js +7 -5
  47. package/src/utils/swigInterface.js +5 -4
  48. package/src/utils/systemConfig.js +13 -0
  49. package/src/utils/targets.js +11 -0
  50. package/src/utils/wasiHttp.js +50 -0
package/src/bin.js CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { Command, Option } from 'commander';
5
+ import { Option } from 'commander';
6
6
 
7
7
  import { state } from './index.js';
8
+ import { addSystemCommands, createProgram, handleFatal } from './program.js';
8
9
  import createBridgeFile from './actions/createInterface.js';
9
10
  import createLib from './actions/createLib.js';
10
11
  import buildWasm from './actions/buildWasm.js';
@@ -15,8 +16,8 @@ import buildExternal from './actions/buildExternal.js';
15
16
  import buildPackageTypes from './actions/buildTypes.js';
16
17
  import buildLib from './actions/buildLib.js';
17
18
  import buildDependencies from './actions/buildDependencies.js';
18
- import writeRuntimeEntry, { removeImportHooks, writeImportHooks } from './actions/writeRuntimeEntry.js';
19
- import { createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
19
+ import writeRuntimeEntry, { removeImportHooks, writeImportHooks, writeImportManifest } from './actions/writeRuntimeEntry.js';
20
+ import { bindsAppImports, createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
20
21
  import { nativeExtensions } from './utils/nativeImports.js';
21
22
  import getDependFilePath from './integration/getDependFilePath.js';
22
23
  import { boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders } from './utils/boundHeaders.js';
@@ -28,18 +29,11 @@ import { OPT_IN_PLATFORMS, selectRuntimeEnvs } from './utils/targets.js';
28
29
  import { resolveHostPlatform } from './utils/hostPlatform.js';
29
30
  import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from './utils/resolveEmbindNapi.js';
30
31
 
31
- import writeJson from './utils/writeJson.js';
32
- import flattenConfigForTable from './utils/flattenConfigForTable.js';
33
- import systemKeys from './utils/systemKeys.js';
34
32
  import logger from './utils/logger.js';
35
33
  import { getDockerImage, getDockerContainerName } from './utils/pullDockerImage.js';
36
34
  import {
37
- RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner, stopRunner,
35
+ RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
38
36
  } from './actions/runnerCommands.js';
39
- import {
40
- deleteAccount as deleteCloudAccount, fetchUsage, login as loginToCloud, logout as logoutOfCloud, usageLines,
41
- } from './actions/cloudCommands.js';
42
- import { cloudUrl } from './utils/cloudAccount.js';
43
37
  import { cargoHome } from './utils/runCargo.js';
44
38
  import { conanRoot } from './utils/runConan.js';
45
39
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
@@ -48,36 +42,16 @@ import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './util
48
42
  import { SBOM_FILE, nodePackageMeta, writeNodePackageLicense } from './utils/packageLicense.js';
49
43
  import findFiles from './utils/findFiles.js';
50
44
 
51
- const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
52
-
53
45
  const platforms = [...new Set(state.targets.map(target => target.platform).filter(t => t))];
54
46
  const archs = [...new Set(state.targets.map(target => target.arch).filter(t => t))];
55
47
  const runtimes = [...new Set(state.targets.map(target => target.runtime).filter(t => t))];
56
48
  const buildTypes = [...new Set(state.targets.map(target => target.buildType).filter(t => t))];
57
49
  const runtimeEnvs = [...new Set(state.targets.map(target => target.runtimeEnv).filter(t => t))];
58
50
 
59
- // Build failures bubble up as exceptions with a `cause` chain (see actions/run.js);
60
- // show the operator a single line — DEBUG=1 for the full stack — and exit non-zero.
61
- const handleFatal = (e) => {
62
- if (process.env.DEBUG) {
63
- console.error(e);
64
- } else {
65
- let message = e?.message || String(e);
66
- if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
67
- console.error(message);
68
- }
69
- process.exit(1);
70
- };
71
51
  process.on('uncaughtException', handleFatal);
72
52
  process.on('unhandledRejection', handleFatal);
73
53
 
74
- const program = new Command();
75
-
76
- program
77
- .name('crossbind')
78
- .description('Compile C++ files to WebAssembly and native platforms.')
79
- .version(packageJSON.version)
80
- .showHelpAfterError();
54
+ const program = createProgram();
81
55
 
82
56
  program.command('build')
83
57
  .description('compile the project that was set up using crossbind')
@@ -87,8 +61,9 @@ program.command('build')
87
61
  .addOption(new Option('-b, --build-type <buildType>', 'target build type').argParser(createListParser(buildTypes)))
88
62
  .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'binaries to make, by runtime environment (without it, only the archives are built)').argParser(createListParser(runtimeEnvs)))
89
63
  .option('--rebuild-deps [list]', 'rebuild dependencies from source instead of using prebuilt (all, or a comma-separated list of names)')
64
+ .option('--bind-imports', "also bind what the app's JavaScript imports into browser binaries (node binaries always do), for a page that resolves those imports itself")
90
65
  .action((options) => {
91
- const { rebuildDeps, ...rest } = options;
66
+ const { rebuildDeps, bindImports, ...rest } = options;
92
67
  const targetParams = { ...rest };
93
68
 
94
69
  if (!targetParams.arch) {
@@ -109,7 +84,7 @@ program.command('build')
109
84
  if (state.config.build.withBuildConfig) {
110
85
  buildExternal(targetParams);
111
86
  } else {
112
- build(targetParams, rebuildDeps, binaryRuntimeEnvs);
87
+ build(targetParams, rebuildDeps, binaryRuntimeEnvs, { bindImports });
113
88
  }
114
89
  });
115
90
 
@@ -284,10 +259,10 @@ commandRunner.command('start')
284
259
  connectCommands(options.role, runner.url, runner.displayToken).forEach((line) => console.log(` ${line}`));
285
260
  }));
286
261
 
287
- commandRunner.command('stop')
288
- .description('stop and remove the local runner')
262
+ commandRunner.command('delete')
263
+ .description('stop and delete the local runner')
289
264
  .addOption(roleOption())
290
- .action(runnerAction((options) => stopRunner({ role: options.role })));
265
+ .action(runnerAction((options) => deleteRunner({ role: options.role })));
291
266
 
292
267
  commandRunner.command('init')
293
268
  .description('write a folder that deploys a runner to a hosting platform')
@@ -306,76 +281,7 @@ commandRunner.command('init')
306
281
  deploySteps(platform, dir, newRunnerToken(), options.role, url).forEach((step) => console.log(` ${step}`));
307
282
  }));
308
283
 
309
- program.command('login')
310
- .description('sign in to crossbind cloud with your GitHub account')
311
- .action(async () => {
312
- const url = cloudUrl(state.config.system);
313
- const { login } = await loginToCloud({ url });
314
- console.log(`crossbind: signed in to ${url} as ${login}.`);
315
- });
316
-
317
- program.command('logout')
318
- .description('sign out of crossbind cloud on this machine')
319
- .option('--all', 'sign out every machine signed in to this account')
320
- .action(async (options) => {
321
- const url = cloudUrl(state.config.system);
322
- const result = await logoutOfCloud({ url, all: Boolean(options.all) });
323
- if (!result) {
324
- console.log(`crossbind: not signed in to ${url}.`);
325
- } else if (result.revoked) {
326
- console.log(`crossbind: signed ${result.login} out${options.all ? ' on every machine' : ''}.`);
327
- } else {
328
- console.warn(`crossbind: signed out here, but crossbind cloud did not revoke the token (${result.reason.replace(/^crossbind: /, '')}). Sign in again and run crossbind logout --all to revoke it.`);
329
- }
330
- });
331
-
332
- program.command('usage')
333
- .description('show what you used of crossbind cloud this month and what is left')
334
- .action(async () => {
335
- usageLines(await fetchUsage({ url: cloudUrl(state.config.system) })).forEach((line) => console.log(line));
336
- });
337
-
338
- program.command('account')
339
- .description('manage your crossbind cloud account')
340
- .command('delete')
341
- .description('delete your crossbind cloud account, with the sign-in of every machine (https://crossbind.dev/privacy/)')
342
- .option('--yes', 'delete it; without this, nothing is deleted')
343
- .action(async (options) => {
344
- const url = cloudUrl(state.config.system);
345
- if (!options.yes) {
346
- throw new Error(`crossbind: this deletes your crossbind cloud account at ${url}, with the sign-in of every machine. Run crossbind account delete --yes to go ahead.`);
347
- }
348
- const { login } = await deleteCloudAccount({ url });
349
- console.log(`crossbind: deleted the crossbind cloud account of ${login}.`);
350
- });
351
-
352
- const commandConfig = program.command('config')
353
- .description('manage the crossbind configuration files');
354
-
355
- commandConfig.command('get')
356
- .description('get the crossbind system configuration')
357
- .argument('<key>', 'configuration key to read')
358
- .action((key) => getSystemConfig(key));
359
-
360
- commandConfig.command('set')
361
- .description('set the crossbind system configuration')
362
- .argument('<key>', 'configuration key to set')
363
- .argument('<value>', 'value to assign')
364
- .action((key, value) => setSystemConfig(key, value));
365
-
366
- commandConfig.command('delete')
367
- .description('delete the crossbind system configuration')
368
- .argument('<key>', 'configuration key to remove')
369
- .action((key) => deleteSystemConfig(key));
370
-
371
- commandConfig.command('list')
372
- .description('list the crossbind configurations')
373
- .addOption(new Option('-t, --type <type>', 'config type').default('system').choices(['all', 'system', 'project']))
374
- .action((options) => listSystemConfig(options.type));
375
-
376
- commandConfig.command('keys')
377
- .description('list all available system configuration keys for crossbind')
378
- .action(() => listSystemKeys());
284
+ addSystemCommands(program);
379
285
 
380
286
  // `pnpm run <script> -- --flag` forwards the `--` literally and commander would then
381
287
  // treat the flags as positionals. Strip the first separator for commands where every
@@ -388,67 +294,6 @@ if (['build', 'clean-deps', 'licenses'].includes(argv[2])) {
388
294
 
389
295
  program.parse(argv);
390
296
 
391
- function listSystemKeys() {
392
- console.info('Available configurations:');
393
- console.table(systemKeys);
394
- }
395
-
396
- function getSystemConfig(systemKey) {
397
- if (!systemKey) {
398
- listSystemConfig('system');
399
- return;
400
- }
401
- const key = systemKey.toUpperCase();
402
- const output = {};
403
- if (key in state.config.system) {
404
- output[key] = { value: state.config.system[key] || 'undefined', default: systemKeys[key].default };
405
- }
406
- Object.keys(systemKeys).filter((k) => k.includes(key)).forEach((k) => {
407
- if (k in state.config.system) {
408
- output[k] = { value: state.config.system[k] || 'undefined', default: systemKeys[k].default };
409
- }
410
- });
411
-
412
- console.table(output);
413
- }
414
-
415
- function setSystemConfig(key, value) {
416
- if (!systemKeys[key]) {
417
- throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
418
- }
419
- if (systemKeys[key].options && !systemKeys[key].options.includes(value)) {
420
- throw new Error(`Value ${value} is not available. Please choose from the following available values: ${systemKeys[key].options.join(', ')}`);
421
- }
422
-
423
- state.config.system[key] = value;
424
- writeJson(state.config.paths.systemConfig, state.config.system);
425
- }
426
-
427
- function deleteSystemConfig(key) {
428
- if (!systemKeys[key]) {
429
- throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
430
- }
431
- delete state.config.system[key];
432
- writeJson(state.config.paths.systemConfig, state.config.system);
433
- }
434
-
435
- function listSystemConfig(type) {
436
- const { system: systemConfig, ...projectConfig } = state.config;
437
- if (type === 'all' || type === 'system') {
438
- console.log('System Configuration');
439
- console.table(systemConfig);
440
- }
441
-
442
- if (type === 'all') {
443
- console.log('');
444
- }
445
-
446
- if (type === 'all' || type === 'project') {
447
- console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
448
- console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
449
- }
450
- }
451
-
452
297
  function run(programName, params) {
453
298
  runCrossbindApp(programName, params, null, null, { console: true });
454
299
  }
@@ -461,7 +306,7 @@ function onlyLoadsPackagedAddons(binaryParams) {
461
306
  return targets.every((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm') && addonsInPackages();
462
307
  }
463
308
 
464
- async function build(targetParams, rebuildOption, binaryRuntimeEnvs) {
309
+ async function build(targetParams, rebuildOption, binaryRuntimeEnvs, wasmOptions = {}) {
465
310
  await buildDependencies({ targetParams, rebuildOption });
466
311
  const binaryParams = { ...targetParams, runtimeEnv: binaryRuntimeEnvs };
467
312
  if (!onlyLoadsPackagedAddons(binaryParams)) {
@@ -470,7 +315,7 @@ async function build(targetParams, rebuildOption, binaryRuntimeEnvs) {
470
315
  if (binaryRuntimeEnvs.length === 0) {
471
316
  logger.info(`crossbind: no runtime environment was selected, so only the archives were built - pass -e (${runtimeEnvs.join(', ')}) for a binary.`);
472
317
  } else {
473
- await createWasmJs(binaryParams);
318
+ await createWasmJs(binaryParams, wasmOptions);
474
319
  await createNodeAddons(binaryParams);
475
320
  await createNativeCommands(binaryParams);
476
321
  await createWasiCommands(binaryParams);
@@ -593,16 +438,15 @@ function scopeOutputToCommonJs() {
593
438
  }
594
439
  }
595
440
 
596
- async function createWasmJs(targetParams) {
441
+ async function createWasmJs(targetParams, wasmOptions = {}) {
597
442
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
598
443
  if (targets.length === 0) {
599
444
  return;
600
445
  }
601
446
  const headers = createBridges();
602
- // A Node app imports what it uses through the module hooks, so a Node binary binds those imports too.
603
- const isNode = (target) => target.runtimeEnv === 'node';
604
- const imports = targets.some(isNode) ? findAppNativeImports([state.targets.find((t) => t.platform === 'wasm')]) : new Map();
605
- const nodeHeaders = [...headers, ...createImportedBridges(headers, imports)];
447
+ const bindsImports = (target) => bindsAppImports(target, wasmOptions);
448
+ const imports = targets.some(bindsImports) ? findAppNativeImports([state.targets.find((t) => t.platform === 'wasm')]) : new Map();
449
+ const appHeaders = [...headers, ...createImportedBridges(headers, imports)];
606
450
  const optOf = (bridged) => ({
607
451
  buildSource: false,
608
452
  nativeGlob: [
@@ -619,7 +463,7 @@ async function createWasmJs(targetParams) {
619
463
  // link and the copy step below moved build/data away on the last run.
620
464
  const distComplete = fs.existsSync(`${state.config.paths.output}/${target.jsName}`)
621
465
  && fs.existsSync(`${state.config.paths.output}/${target.wasmName}`);
622
- createLib(target, 'Bridge', optOf(isNode(target) ? nodeHeaders : headers));
466
+ createLib(target, 'Bridge', optOf(bindsImports(target) ? appHeaders : headers));
623
467
  const built = await buildWasm(target, { force: !distComplete });
624
468
  if (!built) {
625
469
  continue;
@@ -640,15 +484,21 @@ async function createWasmJs(targetParams) {
640
484
  if (targets.some((target) => ['node', 'edge'].includes(target.runtimeEnv))) {
641
485
  scopeOutputToCommonJs();
642
486
  }
487
+ // The list a page that maps the app's imports itself reads; a build without --bind-imports is not for one.
488
+ if (targets.some((target) => target.runtimeEnv === 'browser')) {
489
+ const manifest = `${state.config.paths.output}/browser/imports.json`;
490
+ if (wasmOptions.bindImports) writeImportManifest(manifest, appHeaders, imports, state.config.paths.project);
491
+ else fs.rmSync(manifest, { force: true });
492
+ }
643
493
  // A runtime's entry loads its single-thread wasm32 release build.
644
494
  const entryTargetOf = (runtimeEnv) => targets.find((target) => target.runtimeEnv === runtimeEnv && target.arch === 'wasm32'
645
495
  && target.runtime === 'st' && target.buildType === 'release' && fs.existsSync(`${state.config.paths.output}/${target.jsName}`));
646
496
  const nodeTarget = entryTargetOf('node');
647
497
  const edgeTarget = entryTargetOf('edge');
648
- if (nodeHeaders.length && nodeTarget) {
498
+ if (appHeaders.length && nodeTarget) {
649
499
  const entry = `${state.config.paths.output}/node/wasm.mjs`;
650
- writeRuntimeEntry(entry, nodeHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
651
- if (imports.size) writeImportHooks(entry, nodeHeaders, imports, nativeExtensions(state.config.ext));
500
+ writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
501
+ if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
652
502
  else removeImportHooks(entry);
653
503
  }
654
504
  if (headers.length && edgeTarget) {
package/src/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { addSystemCommands, createProgram, handleFatal } from './program.js';
4
+
5
+ // bin.js loads the project before it reads the command line, which fails outside a project; these commands need none.
6
+ const program = createProgram();
7
+ addSystemCommands(program);
8
+ const withoutProject = [...program.commands.map((command) => command.name()), '-V', '--version'];
9
+
10
+ if (withoutProject.includes(process.argv[2])) {
11
+ await program.parseAsync().catch(handleFatal);
12
+ } else {
13
+ await import('./bin.js');
14
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import getData from '../actions/getData.js';
3
+ import getEnv from '../actions/getEnv.js';
4
4
  import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
@@ -70,6 +70,7 @@ export function getRustSymbols(rsFile) {
70
70
  modulePath,
71
71
  spec: cargoDeps[crateName],
72
72
  cacheDir: state.config.paths.cache,
73
+ projectDir: state.config.paths.project,
73
74
  dtsMode: state.config.dts,
74
75
  log: () => {},
75
76
  });
@@ -110,7 +111,7 @@ function buildScript(target, symbols, base) {
110
111
  if (!target) {
111
112
  throw new Error('The target is not available!');
112
113
  }
113
- const env = JSON.stringify(getData('env', target));
114
+ const env = JSON.stringify(getEnv(target));
114
115
  const getPlatformScript = target.platform === 'wasm' ? getWebScript : getReactNativeScript;
115
116
 
116
117
  let symbolExportDefineString = '';
@@ -178,7 +179,7 @@ function getReactNativeScript(env) {
178
179
  const RNJsiLib = TurboModuleRegistry.get('RNJsiLib');
179
180
 
180
181
  function setEnv() {
181
- const env = JSON.parse('${env}');
182
+ const env = ${env};
182
183
  const CROSSBIND_DATA_PATH = Module.Crossbind.getEnv('CROSSBIND_DATA_PATH');
183
184
 
184
185
  Object.entries(env).forEach(([key, value]) => {
package/src/program.js ADDED
@@ -0,0 +1,169 @@
1
+ import fs from 'node:fs';
2
+ import { Command, Option } from 'commander';
3
+
4
+ import {
5
+ deleteAccount as deleteCloudAccount, fetchUsage, login as loginToCloud, logout as logoutOfCloud, usageLines,
6
+ } from './actions/cloudCommands.js';
7
+ import { cloudUrl } from './utils/cloudAccount.js';
8
+ import flattenConfigForTable from './utils/flattenConfigForTable.js';
9
+ import loadSystemConfig, { systemConfigPath } from './utils/systemConfig.js';
10
+ import systemKeys from './utils/systemKeys.js';
11
+ import writeJson from './utils/writeJson.js';
12
+
13
+ const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
14
+
15
+ // Build failures bubble up as exceptions with a `cause` chain (see actions/run.js);
16
+ // show the operator a single line — DEBUG=1 for the full stack — and exit non-zero.
17
+ export const handleFatal = (e) => {
18
+ if (process.env.DEBUG) {
19
+ console.error(e);
20
+ } else {
21
+ let message = e?.message || String(e);
22
+ if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
23
+ console.error(message);
24
+ }
25
+ process.exit(1);
26
+ };
27
+
28
+ export const createProgram = () => new Command()
29
+ .name('crossbind')
30
+ .description('Compile C++ files to WebAssembly and native platforms.')
31
+ .version(packageJSON.version)
32
+ .showHelpAfterError();
33
+
34
+ export function addSystemCommands(program) {
35
+ program.command('login')
36
+ .description('sign in to crossbind cloud with your GitHub account')
37
+ .action(async () => {
38
+ const url = cloudUrl(loadSystemConfig());
39
+ const { login } = await loginToCloud({ url });
40
+ console.log(`crossbind: signed in to ${url} as ${login}.`);
41
+ });
42
+
43
+ program.command('logout')
44
+ .description('sign out of crossbind cloud on this machine')
45
+ .option('--all', 'sign out every machine signed in to this account')
46
+ .action(async (options) => {
47
+ const url = cloudUrl(loadSystemConfig());
48
+ const result = await logoutOfCloud({ url, all: Boolean(options.all) });
49
+ if (!result) {
50
+ console.log(`crossbind: not signed in to ${url}.`);
51
+ } else if (result.revoked) {
52
+ console.log(`crossbind: signed ${result.login} out${options.all ? ' on every machine' : ''}.`);
53
+ } else {
54
+ console.warn(`crossbind: signed out here, but crossbind cloud did not revoke the token (${result.reason.replace(/^crossbind: /, '')}). Sign in again and run crossbind logout --all to revoke it.`);
55
+ }
56
+ });
57
+
58
+ program.command('usage')
59
+ .description('show what you used of crossbind cloud this month and what is left')
60
+ .action(async () => {
61
+ usageLines(await fetchUsage({ url: cloudUrl(loadSystemConfig()) })).forEach((line) => console.log(line));
62
+ });
63
+
64
+ program.command('account')
65
+ .description('manage your crossbind cloud account')
66
+ .command('delete')
67
+ .description('delete your crossbind cloud account, with the sign-in of every machine (https://crossbind.dev/privacy/)')
68
+ .option('--yes', 'delete it; without this, nothing is deleted')
69
+ .action(async (options) => {
70
+ const url = cloudUrl(loadSystemConfig());
71
+ if (!options.yes) {
72
+ throw new Error(`crossbind: this deletes your crossbind cloud account at ${url}, with the sign-in of every machine. Run crossbind account delete --yes to go ahead.`);
73
+ }
74
+ const { login } = await deleteCloudAccount({ url });
75
+ console.log(`crossbind: deleted the crossbind cloud account of ${login}.`);
76
+ });
77
+
78
+ const commandConfig = program.command('config')
79
+ .description('manage the crossbind configuration files');
80
+
81
+ commandConfig.command('get')
82
+ .description('get the crossbind system configuration')
83
+ .argument('<key>', 'configuration key to read')
84
+ .action((key) => getSystemConfig(key));
85
+
86
+ commandConfig.command('set')
87
+ .description('set the crossbind system configuration')
88
+ .argument('<key>', 'configuration key to set')
89
+ .argument('<value>', 'value to assign')
90
+ .action((key, value) => setSystemConfig(key, value));
91
+
92
+ commandConfig.command('delete')
93
+ .description('delete the crossbind system configuration')
94
+ .argument('<key>', 'configuration key to remove')
95
+ .action((key) => deleteSystemConfig(key));
96
+
97
+ commandConfig.command('list')
98
+ .description('list the crossbind configurations')
99
+ .addOption(new Option('-t, --type <type>', 'config type').default('system').choices(['all', 'system', 'project']))
100
+ .action((options) => listSystemConfig(options.type));
101
+
102
+ commandConfig.command('keys')
103
+ .description('list all available system configuration keys for crossbind')
104
+ .action(() => listSystemKeys());
105
+ }
106
+
107
+ function listSystemKeys() {
108
+ console.info('Available configurations:');
109
+ console.table(systemKeys);
110
+ }
111
+
112
+ async function getSystemConfig(systemKey) {
113
+ if (!systemKey) {
114
+ await listSystemConfig('system');
115
+ return;
116
+ }
117
+ const system = loadSystemConfig();
118
+ const key = systemKey.toUpperCase();
119
+ const output = {};
120
+ if (key in system) {
121
+ output[key] = { value: system[key] || 'undefined', default: systemKeys[key].default };
122
+ }
123
+ Object.keys(systemKeys).filter((k) => k.includes(key)).forEach((k) => {
124
+ if (k in system) {
125
+ output[k] = { value: system[k] || 'undefined', default: systemKeys[k].default };
126
+ }
127
+ });
128
+
129
+ console.table(output);
130
+ }
131
+
132
+ function setSystemConfig(key, value) {
133
+ if (!systemKeys[key]) {
134
+ throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
135
+ }
136
+ if (systemKeys[key].options && !systemKeys[key].options.includes(value)) {
137
+ throw new Error(`Value ${value} is not available. Please choose from the following available values: ${systemKeys[key].options.join(', ')}`);
138
+ }
139
+
140
+ writeJson(systemConfigPath(), { ...loadSystemConfig(), [key]: value });
141
+ }
142
+
143
+ function deleteSystemConfig(key) {
144
+ if (!systemKeys[key]) {
145
+ throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
146
+ }
147
+ const { [key]: deleted, ...system } = loadSystemConfig();
148
+ writeJson(systemConfigPath(), system);
149
+ }
150
+
151
+ async function listSystemConfig(type) {
152
+ // Importing the state loads the project, which fails outside one, so only a listing of the project does.
153
+ const { system: systemConfig, ...projectConfig } = type === 'system'
154
+ ? { system: loadSystemConfig() }
155
+ : (await import('./state/index.js')).default.config;
156
+ if (type === 'all' || type === 'system') {
157
+ console.log('System Configuration');
158
+ console.table(systemConfig);
159
+ }
160
+
161
+ if (type === 'all') {
162
+ console.log('');
163
+ }
164
+
165
+ if (type === 'all' || type === 'project') {
166
+ console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
167
+ console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
168
+ }
169
+ }
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import loadJson from '../utils/loadJson.js';
3
3
  import writeJson from '../utils/writeJson.js';
4
- import { TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf } from '../utils/targets.js';
4
+ import {
5
+ TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf,
6
+ } from '../utils/targets.js';
5
7
  import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
6
8
  import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
7
9
  import loadConfig, { assertBinarySelection } from './loadConfig.js';
@@ -52,8 +54,7 @@ async function initProcessState() {
52
54
  target.dataTxtName = `${state.config.general.name}-${target.path}.${target.runtimeEnv}.data.txt`;
53
55
  }
54
56
  if (target.platform === 'wasi') {
55
- // A wasi build is a single command module - no JS glue, no preload.
56
- target.wasmName = `${state.config.general.name}-${target.path}.wasm`;
57
+ Object.assign(target, wasiNamesOf(target, state.config.general.name));
57
58
  }
58
59
  if (target.runtimeEnv === 'node' && target.platform !== 'wasm') {
59
60
  Object.assign(target, nodeAddonNamesOf(target, state.config.general.name));
@@ -1,6 +1,5 @@
1
- import os from 'node:os';
2
1
  import fs from 'node:fs';
3
- import systemKeys from '../utils/systemKeys.js';
2
+ import loadSystemConfig, { systemConfigPath } from '../utils/systemConfig.js';
4
3
  import loadJs from '../utils/loadJs.js';
5
4
  import {
6
5
  resolveDependencyOverride, resolveDependencyReplace, restampIdentity, resolveExcludedNames, getOverrideKey,
@@ -32,7 +31,7 @@ export function assertBinarySelection(config) {
32
31
  export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
33
32
  const config = await loadJs(configDir, configName) || {};
34
33
  if (config.excludedDependencies?.length) {
35
- logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: false }.');
34
+ logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: { exclude: true } }.');
36
35
  }
37
36
  const overrides = await loadJs(configDir, 'crossbind.overrides');
38
37
  const exclude = resolveExcludedNames(overrides);
@@ -83,14 +82,8 @@ export default async function loadConfig(configDir = process.cwd(), configName =
83
82
  }
84
83
  });
85
84
 
86
- output.paths.systemConfig = `${os.homedir()}/.crossbind.json`;
87
- output.system = loadJson(output.paths.systemConfig) || {};
88
-
89
- Object.entries(systemKeys).forEach(([key, value]) => {
90
- if (!(key in output.system)) {
91
- output.system[key] = value.default;
92
- }
93
- });
85
+ output.paths.systemConfig = systemConfigPath();
86
+ output.system = loadSystemConfig();
94
87
 
95
88
  return output;
96
89
  }
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import state from '../state/index.js';
4
+ import { followCargoLock, recordCargoLock } from './cargoLock.js';
3
5
  import { cargoTripleFor } from './cargoTarget.js';
4
6
  import { isMtWasm, assertMtRustToolchain, cargoTargetDirFor, cargoBuildInvocation } from './rustMt.js';
5
7
  import runCargo from './runCargo.js';
@@ -110,6 +112,7 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
110
112
  manifestPath: `${superDir}/Cargo.toml`,
111
113
  sysroot,
112
114
  });
115
+ followCargoLock(state.config.paths.project, superDir);
113
116
  const build = runCargo(args, {
114
117
  rustflags, panic, allowUnstable, target,
115
118
  });
@@ -118,5 +121,6 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
118
121
  }
119
122
  const libFile = `${targetDir}/${triple}/release/libcrossbind_app_super.a`;
120
123
  if (!fs.existsSync(libFile)) throw new Error(`crossbind: expected cargo output ${libFile} not found`);
124
+ recordCargoLock(state.config.paths.project, superDir);
121
125
  return [libFile];
122
126
  }