crossbind 2.0.0-beta.68 → 2.0.0-beta.72

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 (88) hide show
  1. package/README.md +1 -1
  2. package/package.json +70 -59
  3. package/src/actions/addExtension.js +196 -0
  4. package/src/actions/androidGradle.js +26 -17
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/buildApp.js +177 -74
  8. package/src/actions/buildDependencies.js +3 -3
  9. package/src/actions/buildNode.js +6 -1
  10. package/src/actions/buildWasm.js +0 -9
  11. package/src/actions/createImportedBridges.js +10 -8
  12. package/src/actions/createInterface.js +40 -29
  13. package/src/actions/createLib.js +3 -7
  14. package/src/actions/extensionApi.js +298 -0
  15. package/src/actions/extensionCommands.js +41 -0
  16. package/src/actions/extensionsInApp.js +19 -0
  17. package/src/actions/getAllBridges.js +20 -5
  18. package/src/actions/getDependLibs.js +5 -0
  19. package/src/actions/getLinkInputs.js +2 -1
  20. package/src/actions/gradleProject.js +207 -16
  21. package/src/actions/iosDevice.js +2 -2
  22. package/src/actions/iosSimulator.js +3 -3
  23. package/src/actions/licenses.js +113 -58
  24. package/src/actions/linuxPackages.js +48 -0
  25. package/src/actions/mavenVerification.js +37 -0
  26. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  27. package/src/actions/publish.js +220 -0
  28. package/src/actions/windowsSigning.js +1 -1
  29. package/src/actions/writeRuntimeEntry.js +2 -2
  30. package/src/actions/xcodeProject.js +60 -29
  31. package/src/actions/xcodeTargets.js +122 -0
  32. package/src/actions/xcodebuild.js +65 -0
  33. package/src/app/index.d.ts +144 -132
  34. package/src/app/index.js +147 -185
  35. package/src/assets/configure/install.sh +0 -0
  36. package/src/assets/node-import-hooks.mjs +7 -6
  37. package/src/assets/toolchain-digests.json +20 -20
  38. package/src/bin.js +37 -37
  39. package/src/index.js +4 -3
  40. package/src/integration/getCrossbindScript.js +3 -2
  41. package/src/integration/getDependFilePath.js +10 -10
  42. package/src/program.js +32 -3
  43. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  44. package/src/state/index.js +8 -6
  45. package/src/state/loadConfig.js +26 -9
  46. package/src/state/refreshPackageSources.js +19 -0
  47. package/src/utils/appExtensions.js +777 -0
  48. package/src/utils/appFiles.js +58 -0
  49. package/src/utils/appLaunches.js +249 -0
  50. package/src/utils/appOptions.js +142 -112
  51. package/src/utils/appRustCrates.js +2 -2
  52. package/src/utils/appTargets.js +168 -0
  53. package/src/utils/bridgeTools.js +71 -0
  54. package/src/utils/configChanges.js +30 -0
  55. package/src/utils/configWriter.js +143 -26
  56. package/src/utils/cppDts.js +11 -11
  57. package/src/utils/extensionSteps.js +172 -0
  58. package/src/utils/nativeImports.js +9 -2
  59. package/src/utils/nodeDevBuild.js +2 -1
  60. package/src/utils/openUrl.js +2 -1
  61. package/src/utils/packageSourceApi.js +87 -0
  62. package/src/utils/packageSourceImport.js +69 -0
  63. package/src/utils/packageSourceInstall.js +162 -0
  64. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  65. package/src/utils/packageSources.js +108 -0
  66. package/src/utils/plist.js +5 -0
  67. package/src/utils/printableText.js +5 -0
  68. package/src/utils/resolveAppHost.js +7 -0
  69. package/src/utils/runPackageSource.js +251 -0
  70. package/src/utils/rustBridgeGen.js +3 -3
  71. package/src/utils/systemConfig.js +16 -3
  72. package/src/utils/targetToolchain.js +77 -0
  73. package/vitest.config.js +6 -1
  74. package/src/actions/appSetup.js +0 -116
  75. package/src/actions/appStoreConnect.js +0 -188
  76. package/src/actions/appUpdate.js +0 -68
  77. package/src/actions/extensions.js +0 -7
  78. package/src/actions/googlePlay.js +0 -213
  79. package/src/actions/googleSignIn.js +0 -78
  80. package/src/actions/playSetup.js +0 -19
  81. package/src/actions/publishAndroid.js +0 -102
  82. package/src/actions/publishIos.js +0 -146
  83. package/src/state/refreshConanDependencies.js +0 -17
  84. package/src/utils/conanDependencies.js +0 -66
  85. package/src/utils/conanImport.js +0 -58
  86. package/src/utils/conanInstall.js +0 -154
  87. package/src/utils/conanProfile.js +0 -203
  88. package/src/utils/runConan.js +0 -257
package/src/bin.js CHANGED
@@ -15,13 +15,13 @@ import buildWasiCommand from './actions/buildWasiCommand.js';
15
15
  import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
16
16
  import buildNativeCommand from './actions/buildNativeCommand.js';
17
17
  import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
18
- import { UPDATER_KEY, newUpdaterKey } from './actions/appUpdate.js';
19
18
  import {
20
19
  makeUploadKey, releaseSigningEnv, showUploadKeyPassword, useUploadKey,
21
20
  } from './actions/uploadKey.js';
22
- import { publishAndroid } from './actions/publishAndroid.js';
23
- import { publishIos } from './actions/publishIos.js';
21
+ import publish, { publishHelp, publishersOf } from './actions/publish.js';
24
22
  import { iosDeviceOption } from './actions/iosDevice.js';
23
+ import addExtension from './actions/addExtension.js';
24
+ import addExtensionCommands from './actions/extensionCommands.js';
25
25
  import buildExternal from './actions/buildExternal.js';
26
26
  import buildPackageTypes from './actions/buildTypes.js';
27
27
  import buildLib from './actions/buildLib.js';
@@ -44,7 +44,8 @@ import {
44
44
  RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
45
45
  } from './actions/runnerCommands.js';
46
46
  import { cargoHome } from './utils/runCargo.js';
47
- import { conanRoot } from './utils/runConan.js';
47
+ import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
48
+ import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
48
49
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
49
50
  import collectLicenseRows from './actions/licenses.js';
50
51
  import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
@@ -113,12 +114,18 @@ function declaredSbomPath(projectDir) {
113
114
  const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
114
115
 
115
116
  function projectVite() {
117
+ let manifestPath;
116
118
  try {
117
- const manifest = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
118
- return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
119
+ manifestPath = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
119
120
  } catch {
120
121
  throw new Error("crossbind: crossbind app builds the app's page with the project's Vite, which the project does not have. Add vite and @crossbind/plugin-vite to it, as the desktop-app template has them.");
121
122
  }
123
+ // A workspace can map vite to a package of Vite's API alone.
124
+ const { name, version, bin } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
125
+ if (!bin?.vite) {
126
+ throw new Error(`crossbind: crossbind app runs the vite command, and the project's vite resolves to ${name} ${version}, which has none. Have vite resolve to Vite itself for the project, with an override where its workspace maps vite to another package.`);
127
+ }
128
+ return path.join(path.dirname(manifestPath), bin.vite);
122
129
  }
123
130
 
124
131
  // The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
@@ -167,14 +174,6 @@ const commandApp = program.command('app').description('build the project as an a
167
174
  });
168
175
  });
169
176
 
170
- commandApp.command('updater-key')
171
- .description("make the key pair that signs a desktop app's updates")
172
- .action(() => {
173
- const { publicKey, privateKey } = newUpdaterKey();
174
- console.log(`crossbind: app.updater.publicKey in crossbind.config.js: ${publicKey}`);
175
- console.log(`crossbind: ${UPDATER_KEY}, kept secret where the app is built: ${privateKey}`);
176
- });
177
-
178
177
  commandApp.command('upload-key')
179
178
  .description("make this machine's upload key, which signs an Android app's bundle for Google Play")
180
179
  .option('--use <keystore>', 'make a key you have the upload key instead, once it opens with its password')
@@ -188,26 +187,26 @@ commandApp.command('upload-key')
188
187
  });
189
188
 
190
189
  commandApp.command('publish')
191
- .description('build the app and send it to its store: an Android bundle to a Google Play track, an iOS app to App Store Connect for TestFlight')
192
- .addOption(new Option('-p, --platform <platform>', "the store's platform (default: CROSSBIND_APP_PLATFORM)").choices(['android', 'ios']))
193
- .option('--track <track>', 'the Google Play track the release goes on: internal, alpha, beta, production or one of your own', 'internal')
194
- .option('--draft', 'leave the Google Play release a draft, which Play Console rolls out')
195
- // Vite's own options, as --mode, go on to Vite.
190
+ .usage('[store] [options]')
191
+ .description("build the app and send it to a store, as its publisher does, one of crossbind's own or of an extension's")
192
+ .addOption(new Option('-p, --platform <platform>', "the app's platform: without a store, the one that goes first for it (default: CROSSBIND_APP_PLATFORM, else for a store of desktop apps this machine's)").choices(APP_PLATFORMS))
193
+ .addHelpText('after', () => publishHelp(publishersOf(state.config?.extensions ?? [])))
194
+ // The store's own options, and Vite's, as --mode, which go on to Vite.
196
195
  .allowUnknownOption()
197
196
  .allowExcessArguments()
198
- .action(async ({ platform, track, draft }, command) => {
197
+ .action(async ({ platform }, command) => {
199
198
  assertAppProject();
200
- const store = platform ?? process.env.CROSSBIND_APP_PLATFORM;
201
- if (store === 'android') {
202
- await publishAndroid({ track, status: draft ? 'draft' : 'completed', viteArgs: command.args }, { runVite });
203
- return;
204
- }
205
- if (store !== 'ios') throw new Error('crossbind: crossbind app publish takes -p android or -p ios, the platform of the store it sends the app to.');
206
- const playOptions = ['track', 'draft'].filter((option) => command.getOptionValueSource(option) === 'cli');
207
- if (playOptions.length > 0) {
208
- throw new Error(`crossbind: ${playOptions.map((option) => `--${option}`).join(' and ')} ${playOptions.length > 1 ? 'are' : 'is'} Google Play's; App Store Connect hands every iOS upload to TestFlight.`);
209
- }
210
- await publishIos({ viteArgs: command.args }, { runVite });
199
+ await publish(command.args, { platform, envPlatform: process.env.CROSSBIND_APP_PLATFORM, runVite });
200
+ });
201
+
202
+ // crossbind app alone reads them, so that an extension's package that cannot be read stops no other command.
203
+ if (process.argv[2] === 'app') addExtensionCommands(commandApp);
204
+
205
+ program.command('add')
206
+ .description("install an app extension's package with the project's package manager and list it in crossbind.config.js's extensions")
207
+ .argument('<package>', "the package as a package manager takes it: its name, name@version or a path to it")
208
+ .action((spec) => {
209
+ addExtension(spec, { config: state.config });
211
210
  });
212
211
 
213
212
  program.command('licenses')
@@ -318,17 +317,18 @@ imageArgument(commandDocker.command('create').description('create docker contain
318
317
  .action((image) => {
319
318
  // Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
320
319
  const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
320
+ const sources = sourceNamesOf(state.config);
321
321
  // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
322
- [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
322
+ [cargoHome(), ...sources.map((name) => sourceRoot(name))].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
323
323
  dockerExec([
324
324
  'run', '-dit',
325
325
  ...(platform ? ['--platform', platform] : []),
326
326
  '--name', dockerContainerName(image),
327
327
  '-v', `${state.config.paths.base}:/tmp/crossbind/live`,
328
- // Cargo's registry cache, shared with host builds and with every other project, and the
329
- // Conan store and work directories every other container build uses.
328
+ // Cargo's registry cache, shared with host builds and with every other project, and each package
329
+ // source's store and work directories, which every other container build of it uses.
330
330
  '-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
331
- '-v', `${conanRoot()}:/var/cache/crossbind/conan`,
331
+ ...sources.flatMap((name) => ['-v', `${sourceRoot(name)}:${containerRootOf(name)}`]),
332
332
  getDockerImage(image, platform),
333
333
  'bash',
334
334
  ]);
@@ -535,7 +535,7 @@ async function createNodeAddons(targetParams) {
535
535
  if (headers.length && fs.existsSync(`${state.config.paths.output}/${loaderTarget.jsName}`)) {
536
536
  const entry = `${state.config.paths.output}/node/napi.mjs`;
537
537
  writeRuntimeEntry(entry, headers, ENTRY_RUNTIMES.node(loaderTarget.jsName));
538
- if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext));
538
+ if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
539
539
  else removeImportHooks(entry);
540
540
  }
541
541
  }
@@ -611,7 +611,7 @@ async function createWasmJs(targetParams, wasmOptions = {}) {
611
611
  if (appHeaders.length && nodeTarget) {
612
612
  const entry = `${state.config.paths.output}/node/wasm.mjs`;
613
613
  writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
614
- if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
614
+ if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
615
615
  else removeImportHooks(entry);
616
616
  }
617
617
  if (headers.length && edgeTarget) {
package/src/index.js CHANGED
@@ -4,9 +4,10 @@ export { default as createLib } from './actions/createLib.js';
4
4
  export { default as createBridgeFile } from './actions/createInterface.js';
5
5
  export { default as buildWasm } from './actions/buildWasm.js';
6
6
  export {
7
- default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
7
+ default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams,
8
8
  } from './actions/buildApp.js';
9
- export { checkInstaller } from './actions/appInstaller.js';
9
+ export { checkInstaller, shipApp } from './actions/appInstaller.js';
10
+ export { resolveAppPageApi } from './utils/resolveAppHost.js';
10
11
  export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
11
12
  export {
12
13
  default as openOnIosDevice, buildForIosDevice, connectedIosDevice, iosDeviceAddress, iosDevServer,
@@ -23,7 +24,7 @@ export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredT
23
24
  export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
24
25
  export { default as getDependFilePath } from './integration/getDependFilePath.js';
25
26
  export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
26
- export { default as prepareConanDependencies } from './actions/prepareConanDependencies.js';
27
+ export { default as preparePackageSources } from './actions/preparePackageSources.js';
27
28
  export { default as findFiles } from './utils/findFiles.js';
28
29
  export { getContentHash, getFileHash } from './utils/hash.js';
29
30
  export {
@@ -5,6 +5,7 @@ import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
7
7
  import { parseCargoMarkerName } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
9
10
 
10
11
  // `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
@@ -62,7 +63,7 @@ export function getRustSymbols(rsFile) {
62
63
  // Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
63
64
  // upstream crate's own source, and the bridge is synthesized against the cargo dependency.
64
65
  const { crateName, modulePath } = parseCargoMarkerName(path.basename(rsReal, '.rs'));
65
- const cargoDeps = state.config.cargoDependencies ?? {};
66
+ const cargoDeps = cratesOf(state.config);
66
67
  if (Object.hasOwn(cargoDeps, crateName)
67
68
  && path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
68
69
  const { exports } = createCrateImportBridge({
@@ -92,7 +93,7 @@ export function getRustSymbols(rsFile) {
92
93
  projectPath: state.config.paths.project,
93
94
  dtsMode: state.config.dts,
94
95
  vectors,
95
- cargoDependencies: state.config.cargoDependencies ?? {},
96
+ cargoDependencies: cratesOf(state.config),
96
97
  log: () => {},
97
98
  });
98
99
  }
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import state from '../state/index.js';
4
4
  import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
5
- import { CONAN_PREFIX, resolveConanHeader } from '../utils/conanImport.js';
6
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
+ import { cratesOf, sourceOfImport, resolveSourceHeader } from '../utils/packageSourceImport.js';
6
+ import refreshPackageSources from '../state/refreshPackageSources.js';
7
7
 
8
8
  const LAYOUT = { header: 'include', module: 'swig', source: '' };
9
9
 
@@ -85,20 +85,20 @@ export default function getDependFilePath(source, target) {
85
85
  // an undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
86
86
  if (source.startsWith('cargo:')) {
87
87
  const cargoImport = parseCargoImport(source);
88
- const cargoDeps = state.config.cargoDependencies ?? {};
89
- if (!Object.hasOwn(cargoDeps, cargoImport.crateName)) {
90
- throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to cargoDependencies in crossbind.config.`);
88
+ if (!Object.hasOwn(cratesOf(state.config), cargoImport.crateName)) {
89
+ throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to packages.cargo in crossbind.config.`);
91
90
  }
92
91
  const marker = writeCargoMarker(state.config.paths.cache, cargoImport);
93
92
  existsCache.set(marker, true);
94
93
  return marker;
95
94
  }
96
95
 
97
- // `conan:<package>/<header>` names a header of a declared Conan package, staged per target like a
98
- // port's prebuilt; from there the header takes the same path as any other.
99
- if (source.startsWith(CONAN_PREFIX)) {
100
- refreshConanDependencies(state.config);
101
- return resolveConanHeader(state.config, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
96
+ // `<source>:<package>/<header>`, as conan:zlib/zlib.h, names a header of a declared package of a package
97
+ // source, staged per target like a port's prebuilt; from there the header takes the same path as any other.
98
+ const packageSource = sourceOfImport(state.config, source);
99
+ if (packageSource) {
100
+ refreshPackageSources(state.config);
101
+ return resolveSourceHeader(state.config, packageSource.name, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
102
102
  }
103
103
 
104
104
  const matches = getMatchingPackages(source);
package/src/program.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  } from './actions/cloudCommands.js';
7
7
  import { cloudUrl } from './utils/cloudAccount.js';
8
8
  import flattenConfigForTable from './utils/flattenConfigForTable.js';
9
- import loadSystemConfig, { systemConfigPath } from './utils/systemConfig.js';
9
+ import loadSystemConfig, { extensionSettingsOf, systemConfigPath } from './utils/systemConfig.js';
10
10
  import systemKeys from './utils/systemKeys.js';
11
11
  import writeJson from './utils/writeJson.js';
12
12
 
@@ -19,7 +19,9 @@ export const handleFatal = (e) => {
19
19
  console.error(e);
20
20
  } else {
21
21
  let message = e?.message || String(e);
22
- if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
22
+ // A cause the message tells already, as an extension's failed step does, is told once.
23
+ const cause = e?.cause?.message?.split('\n')[0];
24
+ if (cause && !message.includes(cause)) message += `\n caused by: ${cause}`;
23
25
  console.error(message);
24
26
  }
25
27
  // Writes to a pipe are asynchronous, so exiting at once would cut what is still queued, such as the failed
@@ -111,12 +113,30 @@ function listSystemKeys() {
111
113
  console.table(systemKeys);
112
114
  }
113
115
 
116
+ // An extension's own setting, as its name and the setting's name it: demo-store.TOKEN.
117
+ function extensionSetting(text) {
118
+ const [, extension, key] = /^([a-z][a-z0-9-]*)\.([A-Za-z][A-Za-z0-9_]*)$/.exec(text) ?? [];
119
+ return extension ? { extension, key: key.toUpperCase() } : null;
120
+ }
121
+
122
+ // crossbind's settings, and each extension's own by its name and the setting's.
123
+ function shownSettings(system) {
124
+ const { extensions = {}, ...settings } = system;
125
+ const owned = Object.keys(extensions).flatMap((name) => Object.entries(extensionSettingsOf(system, name)).map(([key, value]) => [`${name}.${key}`, value]));
126
+ return { ...settings, ...Object.fromEntries(owned) };
127
+ }
128
+
114
129
  async function getSystemConfig(systemKey) {
115
130
  if (!systemKey) {
116
131
  await listSystemConfig('system');
117
132
  return;
118
133
  }
119
134
  const system = loadSystemConfig();
135
+ const own = extensionSetting(systemKey);
136
+ if (own) {
137
+ console.table({ [`${own.extension}.${own.key}`]: { value: extensionSettingsOf(system, own.extension)[own.key] ?? 'undefined' } });
138
+ return;
139
+ }
120
140
  const key = systemKey.toUpperCase();
121
141
  const output = {};
122
142
  if (key in system) {
@@ -143,6 +163,15 @@ function setSystemConfig(key, value) {
143
163
  }
144
164
 
145
165
  function deleteSystemConfig(key) {
166
+ const own = extensionSetting(key);
167
+ if (own) {
168
+ const system = loadSystemConfig();
169
+ const { [own.key]: deleted, ...kept } = extensionSettingsOf(system, own.extension);
170
+ if (deleted === undefined) throw new Error(`Configuration ${own.extension}.${own.key} is not set.`);
171
+ const { [own.extension]: emptied, ...others } = system.extensions;
172
+ writeJson(systemConfigPath(), { ...system, extensions: Object.keys(kept).length > 0 ? { ...others, [own.extension]: kept } : others });
173
+ return;
174
+ }
146
175
  if (!systemKeys[key]) {
147
176
  throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
148
177
  }
@@ -157,7 +186,7 @@ async function listSystemConfig(type) {
157
186
  : (await import('./state/index.js')).default.config;
158
187
  if (type === 'all' || type === 'system') {
159
188
  console.log('System Configuration');
160
- console.table(systemConfig);
189
+ console.table(shownSettings(systemConfig));
161
190
  }
162
191
 
163
192
  if (type === 'all') {
@@ -1,8 +1,8 @@
1
1
  import { getFilledConfig, flattenDependencies } from './loadConfig.js';
2
2
  import calculateDependencyParameters from './calculateDependencyParameters.js';
3
- import { conanStageDir, conanPackageDir } from '../utils/conanImport.js';
4
- import { conanDependenciesKey } from '../utils/conanDependencies.js';
5
- import { readConanManifest, conanTargetName } from '../utils/conanStage.js';
3
+ import { sourceStageDir, sourcePackageDir } from '../utils/packageSourceImport.js';
4
+ import { declaredSources, dependenciesKey, dependenciesOf } from '../utils/packageSources.js';
5
+ import { readSourceManifest, sourceTargetName } from '../utils/packageSourceStage.js';
6
6
  import { TARGETS, targetPathOf } from '../utils/targets.js';
7
7
 
8
8
  // What a recipe declares for its consumers, per staged target since it declares other things on another
@@ -32,25 +32,29 @@ function targetSpecsOf(name, targets) {
32
32
  // staged for it, so a name it lacks drops out there.
33
33
  const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
34
34
 
35
- export function conanDependencyConfigs(manifest, stageDir) {
35
+ export function sourceDependencyConfigs(source, manifest, stageDir) {
36
36
  return manifest.packages.map((pkg) => ({
37
- general: { name: conanTargetName(pkg.name), conan: pkg },
37
+ general: { name: sourceTargetName(source, pkg.name), packageSource: { name: source, package: pkg } },
38
38
  export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
39
- paths: { project: conanPackageDir(stageDir, pkg.name), output: `${conanPackageDir(stageDir, pkg.name)}/dist` },
39
+ paths: { project: sourcePackageDir(stageDir, pkg.name), output: `${sourcePackageDir(stageDir, pkg.name)}/dist` },
40
40
  targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
41
41
  }));
42
42
  }
43
43
 
44
- // The staged Conan packages join the config as dependencies, so header resolution, the link, the
45
- // cmake graph and the license rows see them like any port. Only packages staged for the current
46
- // conanDependencies join; reading the manifest runs nothing, so state can do it while it loads.
47
- export default function attachConanDependencies(config) {
48
- const isDeclared = Object.keys(config.conanDependencies ?? {}).length > 0;
49
- if (!isDeclared && !config.dependencies?.some((d) => d.general.conan)) return;
50
- const stageDir = conanStageDir(config.paths.cache);
51
- const manifest = isDeclared ? readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies)) : null;
52
- const conan = manifest ? conanDependencyConfigs(manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
53
- config.dependencies = [...config.dependencies.filter((d) => !d.general.conan), ...conan];
44
+ function stagedDependencies(config, source) {
45
+ const stageDir = sourceStageDir(config.paths.cache, source);
46
+ const manifest = readSourceManifest(source, stageDir, dependenciesKey(dependenciesOf(config, source)));
47
+ return manifest ? sourceDependencyConfigs(source, manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
48
+ }
49
+
50
+ // The staged packages of each source join the config as dependencies, so header resolution, the link, the
51
+ // cmake graph and the license rows see them like any port. Only packages staged for what the config declares
52
+ // now join; reading a manifest runs nothing, so state can do it while it loads.
53
+ export default function attachPackageSources(config) {
54
+ const declared = declaredSources(config);
55
+ if (declared.length === 0 && !config.dependencies?.some((d) => d.general.packageSource)) return;
56
+ const staged = declared.flatMap(({ name }) => stagedDependencies(config, name));
57
+ config.dependencies = [...config.dependencies.filter((d) => !d.general.packageSource), ...staged];
54
58
  config.allDependencies = flattenDependencies(config.dependencies);
55
59
  config.dependencyParameters = calculateDependencyParameters(config);
56
60
  }
@@ -5,9 +5,11 @@ import {
5
5
  TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf, appNamesOf,
6
6
  } from '../utils/targets.js';
7
7
  import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
9
10
  import loadConfig, { assertBinarySelection } from './loadConfig.js';
10
- import refreshConanDependencies from './refreshConanDependencies.js';
11
+ import { configChangesOf } from '../utils/configChanges.js';
12
+ import refreshPackageSources from './refreshPackageSources.js';
11
13
 
12
14
  const cacheDir = `${process.cwd()}/.crossbind`;
13
15
 
@@ -25,9 +27,9 @@ await initProcessState();
25
27
 
26
28
  async function initProcessState() {
27
29
  state.cache = loadCacheState();
28
- state.config = await loadConfig();
30
+ state.config = await loadConfig(undefined, undefined, { changes: configChangesOf() });
29
31
  assertBinarySelection(state.config);
30
- refreshConanDependencies(state.config);
32
+ refreshPackageSources(state.config);
31
33
  exportWasiSdkPath(state.config.system);
32
34
  // Same idea for the docker image overrides: pullDockerImage.js resolves them from the
33
35
  // environment alone, so it stays free of a state import (and of the cycle that would create).
@@ -73,7 +75,7 @@ async function initProcessState() {
73
75
  // Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
74
76
  // its startup file map, so a marker first created mid-resolution is invisible to that very
75
77
  // build. Bare crates come from the config, module imports from the app's own sources.
76
- const cargoDeps = state.config.cargoDependencies ?? {};
78
+ const cargoDeps = cratesOf(state.config);
77
79
  const crateNames = Object.keys(cargoDeps);
78
80
  const moduleImports = crateNames.length && state.config.paths.project
79
81
  ? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
@@ -97,8 +99,8 @@ export function setAllDependecyPaths() {
97
99
  state.config.allDependencyPaths[target.path] = { cmake: {} };
98
100
  state.config.allDependencies.forEach((d) => {
99
101
  state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
100
- // A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
101
- const libNames = d.general.conan
102
+ // A package source's recipe can name a library on another platform only (libpng is png16 on Windows).
103
+ const libNames = d.general.packageSource
102
104
  ? d.export.libName.filter((name) => [target.path, target.releasePath]
103
105
  .some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
104
106
  : d.export.libName;
@@ -12,8 +12,10 @@ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
12
12
  import calculateDependencyParameters from './calculateDependencyParameters.js';
13
13
  import logger from '../utils/logger.js';
14
14
  import { RUNTIME_ENVS } from '../utils/targets.js';
15
- import { normalizeConanDependencies } from '../utils/conanDependencies.js';
15
+ import { checkedPackages, packageSourcesOf } from '../utils/packageSources.js';
16
16
  import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
17
+ import { listedExtension } from '../utils/appExtensions.js';
18
+ import { mergePatch } from '../utils/configChanges.js';
17
19
  // import getCmakeParameters from './getCmakeParameters.js';
18
20
 
19
21
  // For the project being built only: dependency builds load published configs that may still set
@@ -28,15 +30,23 @@ export function assertBinarySelection(config) {
28
30
  }
29
31
  }
30
32
 
31
- export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
32
- const config = await loadJs(configDir, configName) || {};
33
+ // The export each loaded config was filled from, as crossbind read it, which an extension's api shows.
34
+ const exports = new WeakMap();
35
+ export const exportOf = (config) => exports.get(config) ?? null;
36
+
37
+ // The project's config, with the changes a publisher makes for its build, as a JSON merge patch of the export.
38
+ export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config', { changes = null } = {}) {
39
+ const exported = await loadJs(configDir, configName) || {};
40
+ const config = changes ? mergePatch(exported, changes) : exported;
33
41
  if (config.excludedDependencies?.length) {
34
42
  logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: { exclude: true } }.');
35
43
  }
36
44
  const overrides = await loadJs(configDir, 'crossbind.overrides');
37
45
  const exclude = resolveExcludedNames(overrides);
38
46
  const seen = new Set();
39
- const output = getFilledConfig(config, { isDepend: false, exclude, seen, replaces: overrides });
47
+ const output = getFilledConfig(config, {
48
+ isDepend: false, exclude, seen, replaces: overrides, file: `${configDir}/${configName}`,
49
+ });
40
50
  output.excludedDependencies = exclude;
41
51
 
42
52
  const build = await loadJs(configDir, 'crossbind.build');
@@ -85,6 +95,7 @@ export default async function loadConfig(configDir = process.cwd(), configName =
85
95
  output.paths.systemConfig = systemConfigPath();
86
96
  output.system = loadSystemConfig();
87
97
 
98
+ exports.set(output, config);
88
99
  return output;
89
100
  }
90
101
 
@@ -117,8 +128,6 @@ export function getFilledConfig(config, options = { isDepend: false }) {
117
128
  const newConfig = {
118
129
  general: config.general || {},
119
130
  dependencies,
120
- cargoDependencies: config.cargoDependencies || {},
121
- conanDependencies: normalizeConanDependencies(config.conanDependencies),
122
131
  // 'sync' types the direct surface; 'promise' wraps every generated method return
123
132
  // for useWorker-style runtimes (constructors stay sync-typed: await new X()).
124
133
  dts: config.dts || 'sync',
@@ -135,6 +144,7 @@ export function getFilledConfig(config, options = { isDepend: false }) {
135
144
  target: config.target || {},
136
145
  // An app's identity: its bundle id, on iOS the Apple team that signs it, the name it shows and its desktop window.
137
146
  app: config.app || {},
147
+ // What an app's host builds in beside its own extensions.
138
148
  extensions: config.extensions || [],
139
149
  package: null,
140
150
  functions: config.functions || {},
@@ -199,9 +209,16 @@ export function getFilledConfig(config, options = { isDepend: false }) {
199
209
 
200
210
  newConfig.allDependencies = flattenDependencies(newConfig.dependencies);
201
211
 
202
- newConfig.extensions?.forEach(e => {
203
- e?.loadConfig?.after(newConfig);
204
- });
212
+ if (!Array.isArray(newConfig.extensions)) throw new Error('crossbind: "extensions" must be a list of extensions, as their packages\' crossbind.extension.js gives them.');
213
+ newConfig.extensions = newConfig.extensions.map((extension, i) => listedExtension(extension, `extensions[${i}]`, newConfig.paths.base));
214
+ const twice = newConfig.extensions.find(({ name }, i) => newConfig.extensions.findIndex((other) => other.name === name) !== i);
215
+ if (twice) throw new Error(`crossbind: "extensions" lists two extensions named ${twice.name}.`);
216
+
217
+ // What the project declares under packages: cargo's crates, and each package source's packages, as packages.conan,
218
+ // as the source's own check takes them. A package installs nothing for the project that depends on it, so a
219
+ // dependency's own declarations are not read.
220
+ newConfig.packageSources = packageSourcesOf(newConfig.extensions);
221
+ newConfig.packages = checkedPackages(newConfig.packageSources, options.isDepend ? {} : config, options.file);
205
222
 
206
223
  if (newConfig.target.runtime !== 'mt' && newConfig.allDependencies.some((d) => d?.target?.runtime === 'mt')) {
207
224
  newConfig.target.runtime = 'mt';
@@ -0,0 +1,19 @@
1
+ import attachPackageSources from './attachPackageSources.js';
2
+ import { sourceStageDir } from '../utils/packageSourceImport.js';
3
+ import { declaredSources } from '../utils/packageSources.js';
4
+ import { sourceManifestsStamp } from '../utils/packageSourceStage.js';
5
+
6
+ const attachedFrom = new WeakMap();
7
+
8
+ // Attaches what a build staged since this config last attached: a process that loaded state before
9
+ // the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
10
+ // it attached again.
11
+ export default function refreshPackageSources(config) {
12
+ const declared = declaredSources(config);
13
+ if (declared.length === 0) return false;
14
+ const stamp = declared.map(({ name }) => `${name}:${sourceManifestsStamp(sourceStageDir(config.paths.cache, name))}`).join('\n');
15
+ if (attachedFrom.get(config) === stamp) return false;
16
+ attachedFrom.set(config, stamp);
17
+ attachPackageSources(config);
18
+ return true;
19
+ }