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
@@ -2,9 +2,9 @@
2
2
  import fs from 'node:fs';
3
3
  import upath from 'upath';
4
4
  import state, { saveCache } from '../state/index.js';
5
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
+ import refreshPackageSources from '../state/refreshPackageSources.js';
6
6
  import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
7
- import { conanInputsOf } from '../utils/conanDependencies.js';
7
+ import { packageSourceInputsOf, sourceNamesOf } from '../utils/packageSources.js';
8
8
  import { getContentHash, getFileHash } from '../utils/hash.js';
9
9
  import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
10
10
  import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
@@ -12,7 +12,7 @@ import {
12
12
  interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
13
13
  } from '../utils/bridgeExtras.js';
14
14
  import resolveNativeImport from '../integration/resolveNativeImport.js';
15
- import { writeHeaderDts, writeConanImportDts } from '../utils/cppDts.js';
15
+ import { writeHeaderDts, writeSourceImportDts } from '../utils/cppDts.js';
16
16
  import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
17
17
  import {
18
18
  buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
@@ -21,9 +21,12 @@ import {
21
21
  } from '../utils/swigInterface.js';
22
22
  import writeIfChanged from '../utils/writeIfChanged.js';
23
23
  import { withDirLockSync } from '../utils/dirLock.js';
24
- import { conanImportOfHeader } from '../utils/conanImport.js';
24
+ import { sourceImportOfHeader } from '../utils/packageSourceImport.js';
25
25
  import run, { cxxPreprocessorFor } from './run.js';
26
26
  import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
27
+ import {
28
+ loadBridgeTools, runPreprocessor, runSwig, usesBridgeTools,
29
+ } from '../utils/bridgeTools.js';
27
30
 
28
31
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
29
32
  const INTERFACE_FORMAT = 'swig-macros-4';
@@ -50,10 +53,12 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
50
53
  }
51
54
 
52
55
  function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, wholeHeaders = [] } = {}) {
53
- // A header can include a Conan package's headers that a build staged after this process loaded, or belong to a
54
- // dependency built since.
55
- refreshConanDependencies(state.config);
56
+ // A header can include the headers of a package source's package that a build staged after this process loaded,
57
+ // or belong to a dependency built since.
58
+ refreshPackageSources(state.config);
56
59
  refreshBuiltDependencies(state.config);
60
+ // Without bridge-tools on a Mac the macro step would fail quietly, and its interface would be kept without macros.
61
+ if (usesBridgeTools(target)) loadBridgeTools();
57
62
  const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
58
63
  if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
59
64
  fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
@@ -71,7 +76,7 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
71
76
  const { headerPath } = includeLocation(interfaceFilePath, target);
72
77
  const inlined = findSwigInlineIncludes(interfaceFilePath, [state.config, ...state.config.allDependencies], headerPath);
73
78
  const swigView = inlined.length ? writeSwigView(interfaceFilePath, headerPath, inlined) : null;
74
- const sourceHash = [getFileHash(interfaceFilePath), ...conanInputsHash()].join('\n');
79
+ const sourceHash = [getFileHash(interfaceFilePath), ...packageInputsHash()].join('\n');
75
80
  // The names imported by name decide what the bridge adds to SWIG's output, so another one makes another bridge.
76
81
  const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
77
82
  const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
@@ -86,13 +91,14 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
86
91
  exportsFile: `${bridgeFile}.exports.json`,
87
92
  projectPath: state.config.paths.project,
88
93
  cacheDir: state.config.paths.cache,
94
+ sources: sourceNamesOf(state.config),
89
95
  dtsMode: state.config.dts,
90
96
  };
91
97
  writeHeaderDts(dtsOptions);
92
- if (conanImportOfHeader(interfaceFilePath, state.config.paths.cache)) {
93
- dtsOptions.declarationsFile = createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
98
+ if (sourceImportOfHeader(interfaceFilePath, dtsOptions.cacheDir, dtsOptions.sources)) {
99
+ dtsOptions.declarationsFile = createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
94
100
  }
95
- writeConanImportDts(dtsOptions);
101
+ writeSourceImportDts(dtsOptions);
96
102
  }
97
103
  if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
98
104
  // A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
@@ -110,11 +116,11 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
110
116
  return bridgeFile;
111
117
  }
112
118
 
113
- // The Conan packages a header's includes may reach, for the interface and bridge caches: they are
114
- // staged after a header may already be bound, and restaged in place for another version.
115
- function conanInputsHash() {
116
- const conan = conanInputsOf(state.config);
117
- return conan ? [`conan:${getContentHash(JSON.stringify(conan))}`] : [];
119
+ // The packages of package sources a header's includes may reach, for the interface and bridge caches: they
120
+ // are staged after a header may already be bound, and restaged in place for another version.
121
+ function packageInputsHash() {
122
+ const inputs = packageSourceInputsOf(state.config);
123
+ return inputs ? [`packages:${getContentHash(JSON.stringify(inputs))}`] : [];
118
124
  }
119
125
 
120
126
  // A header binds the constants the app imports from it, and so the free functions of a dependency's header: the project's
@@ -154,17 +160,18 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
154
160
  const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
155
161
  const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
156
162
  // A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
157
- // interface the package ships beside the header changing or going away, another imported name, or Conan packages
158
- // its includes may reach being staged or moving to another version must regenerate the interface as well.
163
+ // interface the package ships beside the header changing or going away, another imported name, packages of a
164
+ // package source its includes may reach being staged or moving to another version, or the header included by
165
+ // another path, as another target finds it, must regenerate the interface as well.
159
166
  const fileHash = [
160
- INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
167
+ INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...(headerPath ? [`include:${headerPath}`] : []), ...prelude,
161
168
  ...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
162
169
  ...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
163
170
  ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
164
171
  ...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
165
172
  ...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
166
173
  ...(named.length ? [`named:${named.join(',')}`] : []),
167
- ...conanInputsHash(),
174
+ ...packageInputsHash(),
168
175
  ].join('\n');
169
176
  const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
170
177
  if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
@@ -353,21 +360,25 @@ const RELEASE_DEFINE = '-DNDEBUG';
353
360
 
354
361
  // One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
355
362
  // which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
356
- // still come from the target's own packages.
363
+ // still come from the target's own packages. On a Mac, bridge-tools makes the same bridge, in the build directory the
364
+ // image's tools run in.
357
365
  const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
358
366
 
359
367
  function dumpMacros(outputFile, includes, includePath, target, defines = []) {
360
- const tool = toolTargetFor(target);
361
- run(cxxPreprocessorFor(tool), [
368
+ const args = [
362
369
  '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
363
370
  ...includePath,
364
371
  ...includes.flatMap((header) => ['-include', header]),
365
372
  '-o', outputFile,
366
373
  '/dev/null',
367
- ], null, tool);
374
+ ];
375
+ if (usesBridgeTools(target)) runPreprocessor(args, state.config.paths.build);
376
+ else run(cxxPreprocessorFor(toolTargetFor(target)), args, null, toolTargetFor(target));
368
377
  return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
369
378
  }
370
379
 
380
+ const runSwigFor = (args, target) => (usesBridgeTools(target) ? runSwig(args, state.config.paths.build) : run('swig', args, null, toolTargetFor(target)));
381
+
371
382
  function swigIncludePath(target, sourceDir) {
372
383
  const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
373
384
  const includePath = [
@@ -468,7 +479,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
468
479
  const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
469
480
  fs.writeFileSync(keyOf(bridgeFilePath), '');
470
481
  // #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
471
- const swig = () => run('swig', [
482
+ const swig = () => runSwigFor([
472
483
  '-c++',
473
484
  '-embind',
474
485
  '-cpperraswarn',
@@ -476,7 +487,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
476
487
  ...(swigView ? [`-I${swigView.dir}`] : []),
477
488
  ...swigIncludePath(target, sourceDir),
478
489
  interfaceFilePath,
479
- ], null, toolTargetFor(target));
490
+ ], target);
480
491
  try {
481
492
  swig();
482
493
  } catch (e) {
@@ -508,7 +519,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
508
519
  }
509
520
 
510
521
  // Generate a full export catalog for the editor, without compiling or linking these extra functions.
511
- function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
522
+ function createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
512
523
  const dir = `${state.config.paths.build}/declarations`;
513
524
  fs.mkdirSync(dir, { recursive: true });
514
525
  const file = `${dir}/${upath.basename(interfaceFile)}`;
@@ -517,8 +528,8 @@ function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, s
517
528
  const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
518
529
  if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
519
530
  fs.writeFileSync(keyOf(bridge), '');
520
- run('swig', ['-c++', '-embind', '-cpperraswarn', '-o', bridge,
521
- ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], null, toolTargetFor(target));
531
+ runSwigFor(['-c++', '-embind', '-cpperraswarn', '-o', bridge,
532
+ ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], target);
522
533
  fs.writeFileSync(keyOf(bridge), hash);
523
534
  }
524
535
  return `${bridge}.exports.json`;
@@ -6,7 +6,6 @@ import run from './run.js';
6
6
  import buildCargo from './buildCargo.js';
7
7
  import { cargoTripleFor } from '../utils/cargoTarget.js';
8
8
  import getCmakeParameters from './getCmakeParameters.js';
9
- import triggerExtensions from './extensions.js';
10
9
  import state from '../state/index.js';
11
10
  import logger from '../utils/logger.js';
12
11
  import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
@@ -17,7 +16,7 @@ import {
17
16
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
18
17
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
19
18
  import { targetArchiveFlags } from '../utils/archiveFlags.js';
20
- import { conanInputsOf } from '../utils/conanDependencies.js';
19
+ import { packageSourceInputsOf } from '../utils/packageSources.js';
21
20
  import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
22
21
  import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
23
22
 
@@ -58,10 +57,10 @@ export default function createLib(target, fileType, options = {}) {
58
57
  const configEmccFlags = getData('binary', target)?.emccFlags || [];
59
58
  const compileOptions = getData('cmake', target)?.compileOptions || [];
60
59
  const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
61
- const conanInputs = conanInputsOf(state.config);
60
+ const packageInputs = packageSourceInputsOf(state.config);
62
61
  const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
63
62
  const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
64
- const keyInputs = conanInputs ? [flagsInputs, conanInputs] : flagsInputs;
63
+ const keyInputs = packageInputs ? [flagsInputs, packageInputs] : flagsInputs;
65
64
  // An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
66
65
  const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
67
66
  ? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
@@ -132,7 +131,6 @@ export default function createLib(target, fileType, options = {}) {
132
131
 
133
132
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
134
133
 
135
- triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
136
134
  const archiveFlags = targetArchiveFlags(target);
137
135
  cFlags.push(...archiveFlags);
138
136
  ldFlags.push(...archiveFlags);
@@ -165,8 +163,6 @@ export default function createLib(target, fileType, options = {}) {
165
163
  ];
166
164
  buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
167
165
 
168
- triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
169
-
170
166
  const archiveFlags = targetArchiveFlags(target).join(' ');
171
167
  if (archiveFlags) {
172
168
  buildEnv.params.push('-e', `CFLAGS=${archiveFlags}`);
@@ -0,0 +1,298 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import state from '../state/index.js';
5
+ import { exportOf } from '../state/loadConfig.js';
6
+ import loadSystemConfig, { extensionSettingsOf, updateSystemConfig } from '../utils/systemConfig.js';
7
+ import systemKeys from '../utils/systemKeys.js';
8
+ import { askLine, askYesNo, hasTerminal } from '../utils/ask.js';
9
+ import openUrl from '../utils/openUrl.js';
10
+ import { readSecret, storeSecret } from '../utils/keychain.js';
11
+ import { configChangesOf } from '../utils/configChanges.js';
12
+ import { entry, writeOption } from '../utils/configWriter.js';
13
+ import { readAppIcon, windowsIcon } from '../utils/appIcon.js';
14
+ import {
15
+ PRIVACY_CATEGORIES, androidAppId, appCategory, appDisplayName, appEncryption, appId, appPrivacy, appUrlSchemes,
16
+ } from '../utils/appOptions.js';
17
+ import { isJsonValue, linuxDepends } from '../utils/appExtensions.js';
18
+ import { fileTypeOwners } from '../utils/appLaunches.js';
19
+ import { unreasonedImports } from '../utils/requiredReasonApis.js';
20
+ import { resolveHermesDsym } from '../utils/hermes.js';
21
+ import { imageRoleFor } from '../utils/pullDockerImage.js';
22
+ import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
23
+ import {
24
+ androidVersion, appBuildNumber, appTeam, appVersion,
25
+ } from './buildApp.js';
26
+ import runTool from './run.js';
27
+ import { makeUploadKey, releaseSigningEnv } from './uploadKey.js';
28
+ import { appStoreKey } from './appleSigning.js';
29
+ import { inWindowsImage, jsignScript, windowsSigning } from './windowsSigning.js';
30
+ import { xcodebuild, xcodeTeams } from './xcodebuild.js';
31
+
32
+ // The api an extension's build-side code gets in place of crossbind's own modules, which it never imports: what the run
33
+ // is, the project and its app as the build reads them, the terminal, this machine's settings and keychain, and the
34
+ // signing and tools crossbind builds and packages each platform's apps with.
35
+
36
+ const PATH_PART = /^[A-Za-z_$][\w$]*$/;
37
+ const SETTING = /^[A-Z][A-Z0-9_]*$/;
38
+ const ACCOUNT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
39
+
40
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
41
+
42
+ function refuse() {
43
+ throw new TypeError('crossbind: api.config is read-only: api.config.write writes an option into crossbind.config.js.');
44
+ }
45
+
46
+ const views = new WeakMap();
47
+ const READ_ONLY = {
48
+ get(target, key, receiver) {
49
+ const value = Reflect.get(target, key, receiver);
50
+ const own = Reflect.getOwnPropertyDescriptor(target, key);
51
+ // A proxy gives a property that never changes as it is.
52
+ if (own && !own.configurable && !own.writable) return value;
53
+ return readOnly(value);
54
+ },
55
+ // A configurable property's value is a view too; one that never changes is given as it is, as a proxy must.
56
+ getOwnPropertyDescriptor(target, key) {
57
+ const own = Reflect.getOwnPropertyDescriptor(target, key);
58
+ return own?.configurable && 'value' in own ? { ...own, value: readOnly(own.value) } : own;
59
+ },
60
+ set: refuse,
61
+ defineProperty: refuse,
62
+ deleteProperty: refuse,
63
+ setPrototypeOf: refuse,
64
+ preventExtensions: refuse,
65
+ };
66
+
67
+ function readOnly(value) {
68
+ if (value === null || typeof value !== 'object') return value;
69
+ if (!views.has(value)) views.set(value, new Proxy(value, READ_ONLY));
70
+ return views.get(value);
71
+ }
72
+
73
+ // crossbind.config.js's export as the run's config read it, with the app's options the api wrote since, which the run's
74
+ // config holds: each read sees them, however long ago the extension took the view.
75
+ function configView(write) {
76
+ const current = () => ({ ...exportOf(state.config), app: state.config.app });
77
+ return new Proxy({}, {
78
+ get: (target, key) => (key === 'write' ? write : readOnly(current()[key])),
79
+ has: (target, key) => key in current(),
80
+ ownKeys: () => Reflect.ownKeys(current()),
81
+ getOwnPropertyDescriptor(target, key) {
82
+ const own = Reflect.getOwnPropertyDescriptor(current(), key);
83
+ return own && { ...own, value: readOnly(own.value) };
84
+ },
85
+ set: refuse,
86
+ defineProperty: refuse,
87
+ deleteProperty: refuse,
88
+ setPrototypeOf: refuse,
89
+ preventExtensions: refuse,
90
+ });
91
+ }
92
+
93
+ const deepFrozen = (value) => {
94
+ if (value !== null && typeof value === 'object') Object.values(value).forEach(deepFrozen);
95
+ return Object.freeze(value);
96
+ };
97
+
98
+ const valueAt = (object, keys) => keys.reduce((value, key) => (isObject(value) ? value[key] : undefined), object);
99
+ const withValue = (object, [key, ...rest], value) => ({ ...object, [key]: rest.length === 0 ? value : withValue(isObject(object?.[key]) ? object[key] : {}, rest, value) });
100
+
101
+ // What crossbind checks of an option written into crossbind.config.js, as its builds read it.
102
+ const OPTION_CHECKS = {
103
+ 'app.id': (config, platform) => (platform === 'android' ? androidAppId(config) : appId(config)),
104
+ 'app.name': appDisplayName,
105
+ 'app.icon': readAppIcon,
106
+ 'app.encryption': appEncryption,
107
+ 'app.privacy': appPrivacy,
108
+ };
109
+
110
+ // Writes an option into crossbind.config.js once the user says yes on a terminal, and into this run's config: one of
111
+ // those crossbind checks as the builds would, alone. One the config sets already is the user's to change, and one the
112
+ // builds would refuse fails as invalid, for the caller to ask again.
113
+ async function writeConfig(name, platform, key, value) {
114
+ const keys = typeof key === 'string' ? key.split('.') : [];
115
+ if (keys.length === 0 || !keys.every((part) => PATH_PART.test(part))) throw new Error(`crossbind: ${name} names ${JSON.stringify(key)}, which is no option of crossbind.config.js, as 'app.id' is.`);
116
+ if (!Object.hasOwn(OPTION_CHECKS, key)) {
117
+ const writable = Object.keys(OPTION_CHECKS);
118
+ throw new Error(`crossbind: ${name} writes ${key}, which api.config.write leaves to the app's developer: it writes ${writable.slice(0, -1).join(', ')} and ${writable.at(-1)} alone.`);
119
+ }
120
+ if (!isJsonValue(value)) throw new Error(`crossbind: ${name} gives ${key} a value that is no JSON value, which is all crossbind writes into crossbind.config.js.`);
121
+ // A copy of its own, as the value may hold the api's views of the config.
122
+ const plain = JSON.parse(JSON.stringify(value));
123
+ const text = `${keys.slice(0, -1).map((part) => `${part}.`).join('')}${entry(keys.at(-1), plain)}`;
124
+ if (valueAt(state.config, keys) !== undefined) throw new Error(`crossbind: set ${text} in crossbind.config.js in place of the ${key} there.`);
125
+ const changed = withValue(state.config, keys, plain);
126
+ try {
127
+ OPTION_CHECKS[key]?.(changed, platform);
128
+ } catch (error) {
129
+ throw Object.assign(error, { isInvalid: true });
130
+ }
131
+ if (!hasTerminal()) throw new Error(`crossbind: ${name} would write ${text} into crossbind.config.js, which takes a yes on a terminal: set it there yourself.`);
132
+ if (!await askYesNo(`Write ${text} into crossbind.config.js? [Y/n] `)) throw new Error(`crossbind: set ${text} in crossbind.config.js.`);
133
+ if (!writeOption(state.config.paths.project, keys, plain)) {
134
+ throw new Error(`crossbind: crossbind.config.js is no config crossbind can write ${key} into safely: set ${text} in it yourself.`);
135
+ }
136
+ state.config[keys[0]] = changed[keys[0]];
137
+ }
138
+
139
+ // This machine's settings: crossbind's own by their names, as `crossbind config set` takes them, and by any other name
140
+ // the extension's own, which ~/.crossbind.json keeps apart for each extension.
141
+ function settingsOf(name) {
142
+ const isCrossbinds = (key) => Object.hasOwn(systemKeys, key);
143
+ return Object.freeze({
144
+ get: (key) => (isCrossbinds(key) ? state.config.system[key] : extensionSettingsOf(state.config.system, name)[key]),
145
+ set(values) {
146
+ if (!isObject(values) || !Object.entries(values).every(([key, value]) => SETTING.test(key) && typeof value === 'string')) {
147
+ throw new Error(`crossbind: ${name} sets settings that are no object of this machine's settings, each an uppercase name with a string.`);
148
+ }
149
+ const entries = Object.entries(values);
150
+ entries.filter(([key]) => systemKeys[key]?.options).forEach(([key, value]) => {
151
+ if (!systemKeys[key].options.includes(value)) {
152
+ throw new Error(`crossbind: ${name} sets ${key} to ${JSON.stringify(value)}, which takes ${systemKeys[key].options.join(', ')} alone.`);
153
+ }
154
+ });
155
+ const own = entries.filter(([key]) => !isCrossbinds(key));
156
+ updateSystemConfig(Object.fromEntries(entries.filter(([key]) => isCrossbinds(key))), own.length > 0 ? { name, values: Object.fromEntries(own) } : null);
157
+ state.config.system = loadSystemConfig();
158
+ },
159
+ });
160
+ }
161
+
162
+ // The keychain keeps an extension's secrets under its own service, each by a path the account names, as Windows keeps
163
+ // one in a file beside that path.
164
+ function secretsOf(name) {
165
+ const service = `crossbind ${name}`;
166
+ const keyOf = (account) => {
167
+ if (typeof account !== 'string' || !ACCOUNT.test(account)) throw new Error(`crossbind: ${name} names the secret ${JSON.stringify(account)}, which is no name of letters, digits, dots, dashes and underscores.`);
168
+ return path.join(os.homedir(), '.crossbind', 'secrets', name, account);
169
+ };
170
+ return Object.freeze({
171
+ get: (account) => readSecret(keyOf(account), { service }),
172
+ set(account, secret) {
173
+ const key = keyOf(account);
174
+ if (typeof secret !== 'string' || !secret) throw new Error(`crossbind: ${name} keeps no secret of ${account}, which is no text.`);
175
+ fs.mkdirSync(path.dirname(key), { recursive: true, mode: 0o700 });
176
+ return storeSecret(key, secret, { service });
177
+ },
178
+ });
179
+ }
180
+
181
+ function terminalOf(name) {
182
+ const assertTerminal = (question) => {
183
+ if (!hasTerminal()) throw new Error(`crossbind: ${name} asks ${JSON.stringify(question.trim())}, with no terminal to answer it.`);
184
+ };
185
+ return {
186
+ ask(question) {
187
+ assertTerminal(question);
188
+ return askLine(question);
189
+ },
190
+ askYesNo(question) {
191
+ assertTerminal(question);
192
+ return askYesNo(question);
193
+ },
194
+ };
195
+ }
196
+
197
+ // Runs a tool to its end where crossbind packages an app of the target's platform: on this Mac, with Apple's tools on
198
+ // the PATH, for a macOS app, and for a Linux or Windows app in crossbind's image of its platform, which sees the
199
+ // project's base alone, the windows one with the key that signs the app for a tool that signs. An installer's api alone
200
+ // has the target of the app it makes.
201
+ function runOf(name, target) {
202
+ return (command, options) => {
203
+ if (!target) throw new Error(`crossbind: ${name} runs a tool with api.run, which an installer's api alone has.`);
204
+ if (!Array.isArray(command) || command.length === 0 || !command.every((part) => typeof part === 'string')) {
205
+ throw new Error(`crossbind: ${name} runs no command with api.run, which takes a list of strings: the program and its arguments.`);
206
+ }
207
+ const { cwd = state.config.paths.project, isSigning = false } = options ?? {};
208
+ if (typeof cwd !== 'string') throw new Error(`crossbind: ${name} runs ${command[0]} in ${JSON.stringify(cwd)}, which is no path of a folder.`);
209
+ const dir = path.resolve(state.config.paths.project, cwd);
210
+ if (!fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`crossbind: ${name} runs ${command[0]} in ${dir}, which is no folder.`);
211
+ if (target.platform !== 'darwin' && !isUnderBase(dir, state.config.paths.base)) {
212
+ throw new Error(`crossbind: ${name} runs ${command[0]} in ${dir}, outside ${state.config.paths.base}, the project's base and all the ${imageRoleFor(target)} container sees. Build the app into a folder under it.`);
213
+ }
214
+ if (target.platform === 'win32') inWindowsImage(command, dir, isSigning ? windowsSigning() : null);
215
+ else runTool(null, command, null, target, { workdir: dir });
216
+ };
217
+ }
218
+
219
+ // The api of an extension's run on a platform: the options the command line gave it, the run's build number once the
220
+ // publish picked it, which the app's facts then show, and an installer's target, the app it makes.
221
+ export default function extensionApi(name, {
222
+ platform, options = {}, isGuided, run = {}, target = null,
223
+ }) {
224
+ const buildEnv = () => (run.buildNumber === undefined || run.buildNumber === null
225
+ ? process.env : { ...process.env, CROSSBIND_APP_BUILD_NUMBER: String(run.buildNumber) });
226
+ const config = configView((key, value) => writeConfig(name, platform, key, value));
227
+ return Object.freeze({
228
+ name,
229
+ platform,
230
+ isGuided,
231
+ options: Object.freeze({ ...options }),
232
+ project: Object.freeze({
233
+ get root() { return state.config.paths.project; },
234
+ get name() { return state.config.general.name; },
235
+ get package() { return readOnly(state.config.package); },
236
+ }),
237
+ get workDir() { return path.join(state.config.paths.build, name); },
238
+ config,
239
+ get publisherChanges() { return readOnly(configChangesOf()); },
240
+ app: Object.freeze({
241
+ get id() { return platform === 'android' ? androidAppId(state.config) : appId(state.config); },
242
+ get name() { return appDisplayName(state.config); },
243
+ get version() { return state.config.package?.version ?? '1.0.0'; },
244
+ get appleVersion() { return appVersion(); },
245
+ get versionCode() { return androidVersion(buildEnv()).code; },
246
+ get buildNumber() { return appBuildNumber(buildEnv()); },
247
+ get team() { return appTeam(); },
248
+ get encryption() { return appEncryption(state.config); },
249
+ get urlSchemes() { return Object.freeze(appUrlSchemes(state.config)); },
250
+ get fileTypes() { return Object.freeze(fileTypeOwners(state.config).map(({ extension, ...type }) => Object.freeze(type))); },
251
+ // The Debian packages a Linux app needs beside WebKitGTK and GTK, as its extensions' Linux parts list them.
252
+ get depends() { return Object.freeze(linuxDepends(state.config.extensions ?? [])); },
253
+ get category() {
254
+ const category = appCategory(state.config);
255
+ return category && Object.freeze({ macos: category.darwin, linux: category.linux });
256
+ },
257
+ }),
258
+ log: (text) => console.log(text),
259
+ warn: (text) => console.warn(text),
260
+ ...terminalOf(name),
261
+ openUrl(url) {
262
+ let address = null;
263
+ try {
264
+ address = new URL(url);
265
+ } catch {
266
+ // What is no URL is refused below.
267
+ }
268
+ if (!['http:', 'https:'].includes(address?.protocol)) throw new Error(`crossbind: ${name} opens ${JSON.stringify(url)}, which is no http or https address.`);
269
+ openUrl(address.href);
270
+ },
271
+ run: runOf(name, target),
272
+ settings: settingsOf(name),
273
+ secrets: secretsOf(name),
274
+ android: Object.freeze({
275
+ signing: () => releaseSigningEnv(),
276
+ async makeUploadKey() {
277
+ await makeUploadKey();
278
+ state.config.system = loadSystemConfig();
279
+ },
280
+ }),
281
+ apple: Object.freeze({
282
+ key: () => appStoreKey(),
283
+ teams: () => xcodeTeams(),
284
+ xcodebuild: (what, args, xcodebuildOptions) => xcodebuild(what, args, xcodebuildOptions),
285
+ unreasonedApis: (binary, privacy) => unreasonedImports(binary, privacy),
286
+ privacyCategories: deepFrozen(structuredClone(PRIVACY_CATEGORIES)),
287
+ dsyms: async () => [await resolveHermesDsym()],
288
+ }),
289
+ windows: Object.freeze({
290
+ icon() {
291
+ const icon = readAppIcon(state.config);
292
+ return icon && windowsIcon(icon);
293
+ },
294
+ // The script that signs a file in the windows image with the key, which a tool that signs as it builds runs.
295
+ get signer() { return windowsSigning() ? jsignScript() : null; },
296
+ }),
297
+ });
298
+ }
@@ -0,0 +1,41 @@
1
+ import { Option } from 'commander';
2
+ import extensionApi from './extensionApi.js';
3
+ import state from '../state/index.js';
4
+ import { hasTerminal } from '../utils/ask.js';
5
+ import { extensionStep, ownExtensions, withoutNamed } from '../utils/appExtensions.js';
6
+ import resolveAppHostRoot from '../utils/resolveAppHost.js';
7
+
8
+ const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
9
+
10
+ // crossbind's own extensions a project has, none where its host is not installed, which each command that needs it says:
11
+ // the commands of those app.without leaves out, and of those whose role or name another takes, go with them.
12
+ function ownOf(listed, app) {
13
+ let root;
14
+ try {
15
+ root = resolveAppHostRoot();
16
+ } catch {
17
+ return [];
18
+ }
19
+ const taken = new Set(listed.flatMap(({ name, replaces }) => [name, ...(replaces ?? [])]));
20
+ return withoutNamed(ownExtensions(root), app).filter(({ name }) => !taken.has(name));
21
+ }
22
+
23
+ // Adds to crossbind app the commands of an app's extensions, crossbind's own and those crossbind.config.js lists: a name
24
+ // another has stops the run, naming both.
25
+ export default function addExtensionCommands(parent, config = state.config) {
26
+ if (config?.target?.runtimeEnv !== 'app') return;
27
+ const listed = config.extensions ?? [];
28
+ const owners = new Map(parent.commands.map((command) => [command.name(), 'crossbind']));
29
+ for (const { name: extension, commands = {} } of [...ownOf(listed, config.app), ...listed]) {
30
+ for (const [name, command] of Object.entries(commands)) {
31
+ if (owners.has(name)) throw new Error(`crossbind: ${extension} adds the command crossbind app ${name}, which ${owners.get(name)} has.`);
32
+ owners.set(name, extension);
33
+ const added = parent.command(name).description(command.help);
34
+ Object.entries(command.options).forEach(([option, { help, value, default: fallback }]) => {
35
+ const flag = new Option(`--${kebab(option)}${value ? ` ${value}` : ''}`, help);
36
+ added.addOption(fallback === null ? flag : flag.default(fallback));
37
+ });
38
+ added.action((options) => extensionStep(extension, name, () => command.run(extensionApi(extension, { platform: null, options, isGuided: hasTerminal() }))));
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,19 @@
1
+ import extensionApi from './extensionApi.js';
2
+ import state from '../state/index.js';
3
+ import {
4
+ appExtensionsOf, ownExtensions, withFromApp, withRoles, withoutNamed,
5
+ } from '../utils/appExtensions.js';
6
+ import resolveAppHostRoot from '../utils/resolveAppHost.js';
7
+
8
+ // The platforms as an extension's api names them.
9
+ const API_PLATFORMS = { darwin: 'macos', win32: 'windows' };
10
+
11
+ // The extensions an app of this platform has: crossbind's own but those app.without leaves out, then those
12
+ // crossbind.config.js lists, each with the roles it holds, which a listed one's replaces takes from the host's, and the
13
+ // options it takes from the app's config, where it does.
14
+ export default function extensionsInApp(platform, warn) {
15
+ const own = ownExtensions(resolveAppHostRoot());
16
+ const { app, extensions = [] } = state.config;
17
+ const held = withRoles(appExtensionsOf(withoutNamed(own, app)), appExtensionsOf(extensions), warn, appExtensionsOf(own));
18
+ return withFromApp(held, (name) => extensionApi(name, { platform: API_PLATFORMS[platform] ?? platform, isGuided: false }));
19
+ }
@@ -2,19 +2,34 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import findFiles from '../utils/findFiles.js';
4
4
  import state from '../state/index.js';
5
+ import { declarationOf, dependenciesOf, sourceImportOfHeader } from '../utils/packageSourceImport.js';
6
+
7
+ // Why a bridge's header can no longer be included, or null. A package source's stage outlives the declaration, and
8
+ // the source itself, so the header of a package the project no longer declares is still there while its include
9
+ // directory is off the compile. The stage is named by the path, as the project may have dropped the source too.
10
+ function staleReasonOf(header) {
11
+ if (!fs.existsSync(header)) return 'its header is gone';
12
+ const { config } = state;
13
+ const stages = `${config.paths.cache}/`;
14
+ const [source] = header.startsWith(stages) ? header.slice(stages.length).split('/') : [];
15
+ const staged = source ? sourceImportOfHeader(header, config.paths.cache, [source]) : null;
16
+ if (!staged || Object.hasOwn(dependenciesOf(config, source), staged.name)) return null;
17
+ return `${declarationOf(source)} no longer declares ${staged.name}`;
18
+ }
5
19
 
6
20
  export default function getAllBridges(log = console.log) {
7
21
  return findFiles(`${state.config.paths.build}/bridge/*.i.cpp`).filter((bridge) => {
8
22
  // The .source sidecar records the header this bridge was generated from. When that
9
- // header is gone, the stale .i.cpp would #include a missing file and break the native
10
- // build - the same failure a deleted .rs used to cause. Bridges without a sidecar
11
- // predate it and are left alone.
23
+ // header can no longer be included, the stale .i.cpp would #include a missing file and
24
+ // break the native build - the same failure a deleted .rs used to cause. Bridges without
25
+ // a sidecar predate it and are left alone.
12
26
  const sidecar = `${bridge}.source`;
13
27
  if (!fs.existsSync(sidecar)) return true;
14
28
  const source = fs.readFileSync(sidecar, 'utf8').trim();
15
- if (!source || fs.existsSync(source)) return true;
29
+ const reason = source ? staleReasonOf(source) : null;
30
+ if (!reason) return true;
16
31
  [bridge, `${bridge}.exports.json`, `${bridge}.deps`, `${bridge}.warnings`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
17
- log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - its header is gone`);
32
+ log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - ${reason}`);
18
33
  return false;
19
34
  });
20
35
  }
@@ -21,6 +21,11 @@ export default function getDependLibs(target) {
21
21
  if (libPaths.length === 0) {
22
22
  libPaths = libsIn(target.releasePath);
23
23
  }
24
+ // A published port can leave its iOS archive to its xcframework, where the package's paths lead.
25
+ const xcframeworkLib = target.platform === 'ios' && state.config.allDependencyPaths?.[target.path]?.[fileName]?.lib;
26
+ if (libPaths.length === 0 && xcframeworkLib) {
27
+ libPaths = findFiles(xcframeworkLib, { cwd: d.paths.project });
28
+ }
24
29
  dependLibs.push(...libPaths);
25
30
  });
26
31
  }