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.
- package/README.md +33 -24
- package/package.json +69 -59
- package/src/actions/addExtension.js +191 -0
- package/src/actions/androidGradle.js +69 -25
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/appleSigning.js +32 -14
- package/src/actions/buildApp.js +229 -67
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createInterface.js +17 -8
- package/src/actions/createLib.js +5 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getCmakeParameters.js +2 -3
- package/src/actions/gradleProject.js +199 -16
- package/src/actions/iosDevice.js +200 -0
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +83 -34
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/publish.js +220 -0
- package/src/actions/uploadKey.js +134 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/xcodeProject.js +63 -17
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +126 -133
- package/src/app/index.js +126 -188
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +79 -26
- package/src/index.js +5 -1
- package/src/program.js +35 -4
- package/src/state/index.js +2 -1
- package/src/state/loadConfig.js +16 -5
- package/src/utils/appApiHeader.js +4 -0
- package/src/utils/appExtensions.js +761 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +144 -95
- package/src/utils/appTargets.js +168 -0
- package/src/utils/ask.js +38 -0
- package/src/utils/bridgeTools.js +66 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +181 -0
- package/src/utils/extensionSteps.js +135 -0
- package/src/utils/hermes.js +24 -0
- package/src/utils/keychain.js +54 -0
- package/src/utils/openUrl.js +12 -0
- package/src/utils/password.js +34 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/requiredReasonApis.js +77 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/secretFile.js +23 -0
- package/src/utils/systemConfig.js +19 -0
- package/src/utils/systemKeys.js +16 -0
- package/vitest.config.js +6 -1
- package/src/actions/appUpdate.js +0 -68
- 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
|
|
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
|
-
]
|
|
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 = () =>
|
|
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
|
-
],
|
|
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
|
-
|
|
521
|
-
...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file],
|
|
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`;
|
package/src/actions/createLib.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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
|
|
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}`,
|