crossbind 2.0.0-beta.67 → 2.0.0-beta.69

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 (61) hide show
  1. package/README.md +33 -24
  2. package/package.json +69 -59
  3. package/src/actions/addExtension.js +191 -0
  4. package/src/actions/androidGradle.js +69 -25
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/appleSigning.js +32 -14
  8. package/src/actions/buildApp.js +229 -67
  9. package/src/actions/buildNode.js +6 -1
  10. package/src/actions/buildWasm.js +0 -9
  11. package/src/actions/createInterface.js +17 -8
  12. package/src/actions/createLib.js +5 -7
  13. package/src/actions/extensionApi.js +298 -0
  14. package/src/actions/extensionCommands.js +41 -0
  15. package/src/actions/extensionsInApp.js +19 -0
  16. package/src/actions/getCmakeParameters.js +2 -3
  17. package/src/actions/gradleProject.js +199 -16
  18. package/src/actions/iosDevice.js +200 -0
  19. package/src/actions/iosSimulator.js +3 -3
  20. package/src/actions/licenses.js +83 -34
  21. package/src/actions/linuxPackages.js +48 -0
  22. package/src/actions/mavenVerification.js +37 -0
  23. package/src/actions/publish.js +220 -0
  24. package/src/actions/uploadKey.js +134 -0
  25. package/src/actions/windowsSigning.js +1 -1
  26. package/src/actions/xcodeProject.js +63 -17
  27. package/src/actions/xcodeTargets.js +122 -0
  28. package/src/actions/xcodebuild.js +65 -0
  29. package/src/app/index.d.ts +126 -133
  30. package/src/app/index.js +126 -188
  31. package/src/assets/configure/install.sh +0 -0
  32. package/src/assets/toolchain-digests.json +20 -20
  33. package/src/bin.js +79 -26
  34. package/src/index.js +5 -1
  35. package/src/program.js +35 -4
  36. package/src/state/index.js +2 -1
  37. package/src/state/loadConfig.js +16 -5
  38. package/src/utils/appApiHeader.js +4 -0
  39. package/src/utils/appExtensions.js +761 -0
  40. package/src/utils/appFiles.js +58 -0
  41. package/src/utils/appLaunches.js +249 -0
  42. package/src/utils/appOptions.js +144 -95
  43. package/src/utils/appTargets.js +168 -0
  44. package/src/utils/ask.js +38 -0
  45. package/src/utils/bridgeTools.js +66 -0
  46. package/src/utils/configChanges.js +30 -0
  47. package/src/utils/configWriter.js +181 -0
  48. package/src/utils/extensionSteps.js +135 -0
  49. package/src/utils/hermes.js +24 -0
  50. package/src/utils/keychain.js +54 -0
  51. package/src/utils/openUrl.js +12 -0
  52. package/src/utils/password.js +34 -0
  53. package/src/utils/plist.js +5 -0
  54. package/src/utils/requiredReasonApis.js +77 -0
  55. package/src/utils/resolveAppHost.js +7 -0
  56. package/src/utils/secretFile.js +23 -0
  57. package/src/utils/systemConfig.js +19 -0
  58. package/src/utils/systemKeys.js +16 -0
  59. package/vitest.config.js +6 -1
  60. package/src/actions/appUpdate.js +0 -68
  61. package/src/actions/extensions.js +0 -7
@@ -24,6 +24,9 @@ import { withDirLockSync } from '../utils/dirLock.js';
24
24
  import { conanImportOfHeader } from '../utils/conanImport.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';
@@ -54,6 +57,8 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
54
57
  // dependency built since.
55
58
  refreshConanDependencies(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 });
@@ -353,21 +358,25 @@ const RELEASE_DEFINE = '-DNDEBUG';
353
358
 
354
359
  // One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
355
360
  // 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.
361
+ // still come from the target's own packages. On a Mac, bridge-tools makes the same bridge, in the build directory the
362
+ // image's tools run in.
357
363
  const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
358
364
 
359
365
  function dumpMacros(outputFile, includes, includePath, target, defines = []) {
360
- const tool = toolTargetFor(target);
361
- run(cxxPreprocessorFor(tool), [
366
+ const args = [
362
367
  '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
363
368
  ...includePath,
364
369
  ...includes.flatMap((header) => ['-include', header]),
365
370
  '-o', outputFile,
366
371
  '/dev/null',
367
- ], null, tool);
372
+ ];
373
+ if (usesBridgeTools(target)) runPreprocessor(args, state.config.paths.build);
374
+ else run(cxxPreprocessorFor(toolTargetFor(target)), args, null, toolTargetFor(target));
368
375
  return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
369
376
  }
370
377
 
378
+ const runSwigFor = (args, target) => (usesBridgeTools(target) ? runSwig(args, state.config.paths.build) : run('swig', args, null, toolTargetFor(target)));
379
+
371
380
  function swigIncludePath(target, sourceDir) {
372
381
  const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
373
382
  const includePath = [
@@ -468,7 +477,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
468
477
  const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
469
478
  fs.writeFileSync(keyOf(bridgeFilePath), '');
470
479
  // #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
471
- const swig = () => run('swig', [
480
+ const swig = () => runSwigFor([
472
481
  '-c++',
473
482
  '-embind',
474
483
  '-cpperraswarn',
@@ -476,7 +485,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
476
485
  ...(swigView ? [`-I${swigView.dir}`] : []),
477
486
  ...swigIncludePath(target, sourceDir),
478
487
  interfaceFilePath,
479
- ], null, toolTargetFor(target));
488
+ ], target);
480
489
  try {
481
490
  swig();
482
491
  } catch (e) {
@@ -517,8 +526,8 @@ function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, s
517
526
  const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
518
527
  if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
519
528
  fs.writeFileSync(keyOf(bridge), '');
520
- run('swig', ['-c++', '-embind', '-cpperraswarn', '-o', bridge,
521
- ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], null, toolTargetFor(target));
529
+ runSwigFor(['-c++', '-embind', '-cpperraswarn', '-o', bridge,
530
+ ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], target);
522
531
  fs.writeFileSync(keyOf(bridge), hash);
523
532
  }
524
533
  return `${bridge}.exports.json`;
@@ -6,12 +6,13 @@ 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';
13
12
  import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
14
- import { appApiHeader, compilesAsApp, stageAppApiInclude } from '../utils/appApiHeader.js';
13
+ import {
14
+ appApiHeader, appCompileOptions, compilesAsApp, stageAppApiInclude,
15
+ } from '../utils/appApiHeader.js';
15
16
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
16
17
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
17
18
  import { targetArchiveFlags } from '../utils/archiveFlags.js';
@@ -66,8 +67,8 @@ export default function createLib(target, fileType, options = {}) {
66
67
  : null;
67
68
  // An Android app links the project's archives from the build where React Native's shared library comes from.
68
69
  const appArchives = target.platform === 'android' && target.runtimeEnv === 'app' ? 'archives' : null;
69
- // An app's own code compiles against <crossbind/app.h>, with CROSSBIND_APP defined.
70
- const appApi = compilesAsApp(target) ? getFilesFingerprint([appApiHeader()]) : null;
70
+ // An app's own code compiles against <crossbind/app.h>, with the options of an app.
71
+ const appApi = compilesAsApp(target) ? [getFilesFingerprint([appApiHeader()]), ...appCompileOptions(target)] : null;
71
72
  const extraKeys = [httpHeader, appArchives, appApi].filter(Boolean);
72
73
  const flagsFingerprint = getContentHash(JSON.stringify(extraKeys.length ? [keyInputs, ...extraKeys] : keyInputs));
73
74
  const flagsChanged = !fs.existsSync(flagsFingerprintFile)
@@ -130,7 +131,6 @@ export default function createLib(target, fileType, options = {}) {
130
131
 
131
132
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
132
133
 
133
- triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
134
134
  const archiveFlags = targetArchiveFlags(target);
135
135
  cFlags.push(...archiveFlags);
136
136
  ldFlags.push(...archiveFlags);
@@ -163,8 +163,6 @@ export default function createLib(target, fileType, options = {}) {
163
163
  ];
164
164
  buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
165
165
 
166
- triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
167
-
168
166
  const archiveFlags = targetArchiveFlags(target).join(' ');
169
167
  if (archiveFlags) {
170
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
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import state from '../state/index.js';
3
3
  import getData from './getData.js';
4
- import { compilesAsApp } from '../utils/appApiHeader.js';
4
+ import { appCompileOptions } from '../utils/appApiHeader.js';
5
5
 
6
6
  export default function getCmakeParameters(target, options = {}) {
7
7
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
@@ -39,8 +39,7 @@ export default function getCmakeParameters(target, options = {}) {
39
39
  const otherBuildType = buildType === 'STATIC' ? 'SHARED' : 'STATIC';
40
40
 
41
41
  // An app's own code can tell it is one, to reach <crossbind/app.h>.
42
- const appOptions = compilesAsApp(target) ? ['-DCROSSBIND_APP'] : [];
43
- const cmakeCompileOptions = [...new Set([...(getData('cmake', target)?.compileOptions || []), ...appOptions])];
42
+ const cmakeCompileOptions = [...new Set([...(getData('cmake', target)?.compileOptions || []), ...appCompileOptions(target)])];
44
43
 
45
44
  params.push(...[
46
45
  `-DPROJECT_NAME=${options.name || state.config.general.name}`,