crossbind 2.0.0-beta.68 → 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 +1 -1
- package/package.json +69 -59
- package/src/actions/addExtension.js +191 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +172 -74
- 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 +0 -4
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/gradleProject.js +196 -16
- package/src/actions/iosDevice.js +2 -2
- 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/windowsSigning.js +1 -1
- package/src/actions/xcodeProject.js +49 -24
- 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 +20 -28
- package/src/index.js +2 -1
- package/src/program.js +32 -3
- package/src/state/index.js +2 -1
- package/src/state/loadConfig.js +16 -5
- 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 +109 -100
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +66 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/extensionSteps.js +135 -0
- package/src/utils/openUrl.js +2 -1
- package/src/utils/plist.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/systemConfig.js +16 -3
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
package/src/actions/iosDevice.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { EXEC_MAX_BUFFER } from './run.js';
|
|
5
5
|
import { appTeam } from './buildApp.js';
|
|
6
|
-
import { xcodebuild, xcodeTeams } from './
|
|
6
|
+
import { xcodebuild, xcodeTeams } from './xcodebuild.js';
|
|
7
7
|
import state from '../state/index.js';
|
|
8
8
|
import { appId } from '../utils/appOptions.js';
|
|
9
9
|
|
|
@@ -171,7 +171,7 @@ export function buildForIosDevice(project, device) {
|
|
|
171
171
|
xcodebuild(`the build for ${device.name}`, [
|
|
172
172
|
'-project', `${project}/${name}.xcodeproj`, '-scheme', name, '-configuration', 'Debug', '-sdk', 'iphoneos', '-destination', `id=${device.udid}`,
|
|
173
173
|
'-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration', `SYMROOT=${products}`, 'build',
|
|
174
|
-
], `${products}/device.log`, { team, id: appId(state.config) });
|
|
174
|
+
], { log: `${products}/device.log`, facts: { team, id: appId(state.config) } });
|
|
175
175
|
return `${products}/Debug-iphoneos/${name}.app`;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -43,8 +43,8 @@ export function buildForSimulator(project, configuration = 'Release') {
|
|
|
43
43
|
return `${products}/${configuration}-iphonesimulator/${name}.app`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Opens a Simulator build of an iOS app, with `env` in its environment
|
|
47
|
-
// and ends with the app; close() ends the app.
|
|
46
|
+
// Opens a Simulator build of an iOS app, with `env` in its environment, in the Simulator of the device returned. The
|
|
47
|
+
// process returned carries the app's output and ends with the app; close() ends the app.
|
|
48
48
|
export default function openInSimulator(app, { env = {}, show = false, stdio = 'pipe' } = {}) {
|
|
49
49
|
const device = simulator();
|
|
50
50
|
// Simulator.app only shows the device, which runs the app without it as well.
|
|
@@ -59,5 +59,5 @@ export default function openInSimulator(app, { env = {}, show = false, stdio = '
|
|
|
59
59
|
childEnv[`SIMCTL_CHILD_${key}`] = value;
|
|
60
60
|
});
|
|
61
61
|
const child = spawn('xcrun', ['simctl', 'launch', '--console-pty', '--terminate-running-process', device, bundleId], { env: childEnv, stdio });
|
|
62
|
-
return { child, close: () => spawnSync('xcrun', ['simctl', 'terminate', device, bundleId]) };
|
|
62
|
+
return { child, device, close: () => spawnSync('xcrun', ['simctl', 'terminate', device, bundleId]) };
|
|
63
63
|
}
|
package/src/actions/licenses.js
CHANGED
|
@@ -12,7 +12,11 @@ import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveApp
|
|
|
12
12
|
import {
|
|
13
13
|
HERMES, HERMES_ANDROID, HERMES_SOURCE, hermesUrl,
|
|
14
14
|
} from '../utils/hermes.js';
|
|
15
|
+
import { installersFor } from './appInstaller.js';
|
|
16
|
+
import { DESKTOP_PLATFORMS } from './buildApp.js';
|
|
17
|
+
import extensionsInApp from './extensionsInApp.js';
|
|
15
18
|
import { ANDROIDX_WEBKIT } from './gradleProject.js';
|
|
19
|
+
import { extensionPackage, extensionPart } from '../utils/appExtensions.js';
|
|
16
20
|
import { shippedBundledLicense } from '../utils/bundledLicenses.js';
|
|
17
21
|
import toolchainNoticesDir from '../utils/toolchainNotices.js';
|
|
18
22
|
|
|
@@ -208,13 +212,12 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
208
212
|
}];
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
// What an app's host carries beside crossbind's runtime, pinned as the
|
|
215
|
+
// What an app's host carries beside crossbind's runtime, pinned as the READMEs beside them in @crossbind/core-app-host
|
|
212
216
|
// record: React Native's Hermes, JSI's jsi.cpp from the same release, webview/webview, and on Windows the WebView2 SDK's
|
|
213
217
|
// headers.
|
|
214
218
|
const WEBVIEW = { version: 'cbbdee44afff22867de9fd88a9fc8350d9bdd399', sha256: '10e972a2327b5681474f4aa4499e505eaa4ff659aa995380386f08fd6fc1b763' };
|
|
215
219
|
const WEBVIEW2_SDK = { version: '1.0.4258.31', sha256: '56f7f4b8bf9aee4b8efefbbdd4f67d5f74ebd1b100ed0806da71bf76af481aa9' };
|
|
216
220
|
const JSI_SHA256 = 'cb6274a22abc93ecc970066539815e716d3e47716e8f93a9971cf4eb5918ffc1';
|
|
217
|
-
const MONOCYPHER = { version: '4.0.3', sha256: '8cc9bc341a66249016db9bd70e9142d8d0aef9945973744b1ac05dbc55d8ee66' };
|
|
218
221
|
// The fbjni the Hermes release for Android depends on.
|
|
219
222
|
const FBJNI_VERSION = '0.7.0';
|
|
220
223
|
|
|
@@ -273,8 +276,8 @@ function androidAppRows() {
|
|
|
273
276
|
];
|
|
274
277
|
}
|
|
275
278
|
|
|
276
|
-
// The WebView2 SDK headers a Windows host compiles against
|
|
277
|
-
//
|
|
279
|
+
// The WebView2 SDK headers a Windows host compiles against. webview/webview loads the WebView2 Runtime itself, without the
|
|
280
|
+
// SDK's loader.
|
|
278
281
|
function windowsAppRows(hostRoot) {
|
|
279
282
|
const license = 'BSD-3-Clause';
|
|
280
283
|
return [{
|
|
@@ -290,22 +293,73 @@ function windowsAppRows(hostRoot) {
|
|
|
290
293
|
sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
|
|
291
294
|
licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
|
|
292
295
|
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview2', 'LICENSE')),
|
|
293
|
-
}, {
|
|
294
|
-
npmName: '@crossbind/core-app-host',
|
|
295
|
-
name: 'nsis',
|
|
296
|
-
version: '3.11',
|
|
297
|
-
nativeVersion: '3.11',
|
|
298
|
-
sha256: null,
|
|
299
|
-
license: 'Zlib AND MIT',
|
|
300
|
-
licenseDeclared: 'Zlib AND MIT',
|
|
301
|
-
licenseSelected: null,
|
|
302
|
-
isCopyleft: false,
|
|
303
|
-
sourceUrl: 'https://sources.debian.org/src/nsis/3.11-1/',
|
|
304
|
-
licenseNotes: "the installer stub and plugins the app's setup program carries, from Debian's nsis in the windows toolchain image: zlib/libpng, but for the System plugin's Call.S, MIT",
|
|
305
|
-
licenseText: textOf(path.join(hostRoot, 'third_party', 'nsis', 'LICENSE')),
|
|
306
296
|
}];
|
|
307
297
|
}
|
|
308
298
|
|
|
299
|
+
const APP_PLATFORMS = ['darwin', 'ios', 'android', 'linux', 'win32'];
|
|
300
|
+
|
|
301
|
+
// The extensions an app of this platform has, of any platform's without one: crossbind's own that bring it code, and those
|
|
302
|
+
// crossbind.config.js lists.
|
|
303
|
+
function extensionsIn(platform) {
|
|
304
|
+
if (!platform) return [...new Map(APP_PLATFORMS.flatMap((one) => extensionsIn(one)).map((extension) => [extension.name, extension])).values()];
|
|
305
|
+
const listed = (state.config.extensions ?? []).map(({ name }) => name);
|
|
306
|
+
const hasCode = (extension) => {
|
|
307
|
+
const { native, java } = extensionPart([extension], platform);
|
|
308
|
+
return native.length > 0 || java.length > 0;
|
|
309
|
+
};
|
|
310
|
+
return extensionsInApp(platform).filter((extension) => listed.includes(extension.name) || hasCode(extension));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A row of the code of others an extension carries, by what its licenses list says of it.
|
|
314
|
+
const carriedRow = (entry, root, notes) => ({
|
|
315
|
+
npmName: null,
|
|
316
|
+
name: entry.name,
|
|
317
|
+
version: null,
|
|
318
|
+
nativeVersion: entry.version,
|
|
319
|
+
sha256: entry.sha256 ?? null,
|
|
320
|
+
license: entry.license,
|
|
321
|
+
licenseDeclared: entry.license,
|
|
322
|
+
licenseSelected: null,
|
|
323
|
+
isCopyleft: isCopyleft(entry.license),
|
|
324
|
+
sourceUrl: entry.source ?? null,
|
|
325
|
+
licenseNotes: entry.notes ?? notes,
|
|
326
|
+
licenseText: textOf(path.join(root, entry.file)),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// What the installers of a desktop app carry into what they make.
|
|
330
|
+
const installerRows = (platform) => (DESKTOP_PLATFORMS[platform] ? installersFor({ platform }) : [])
|
|
331
|
+
.flatMap(({ name, root, installer }) => (installer.licenses ?? []).map((entry) => carriedRow(entry, root, `carried by what the installer ${name} makes`)));
|
|
332
|
+
|
|
333
|
+
// What an app's extensions bring into it: each extension package's own code, and the code of others each carries, as its
|
|
334
|
+
// app.licenses lists it. An extension in the project's own folders is the project's code, as the app is.
|
|
335
|
+
function extensionRows(platform) {
|
|
336
|
+
return extensionsIn(platform).flatMap((extension) => {
|
|
337
|
+
const own = extensionPackage(extension.root, state.config.paths.base);
|
|
338
|
+
const owner = own?.manifest.name ?? null;
|
|
339
|
+
const license = own?.manifest.license ?? null;
|
|
340
|
+
const shipped = own && packageLicenseFile(own.dir);
|
|
341
|
+
const packageRows = own && !own.manifest.private ? [{
|
|
342
|
+
npmName: owner,
|
|
343
|
+
name: owner,
|
|
344
|
+
version: own.manifest.version ?? null,
|
|
345
|
+
nativeVersion: null,
|
|
346
|
+
license,
|
|
347
|
+
licenseDeclared: license,
|
|
348
|
+
licenseSelected: null,
|
|
349
|
+
isCopyleft: Boolean(license) && isCopyleft(license),
|
|
350
|
+
sourceUrl: own.manifest.homepage ?? null,
|
|
351
|
+
licenseNotes: `the app extension ${extension.name}, which the app's host compiles in`,
|
|
352
|
+
licenseText: shipped ? textOf(shipped) : null,
|
|
353
|
+
}] : [];
|
|
354
|
+
const carried = (extension.app.licenses ?? []).map((entry) => carriedRow(
|
|
355
|
+
entry,
|
|
356
|
+
extension.root,
|
|
357
|
+
`carried by the app extension ${extension.name}${owner ? ` of ${owner}` : ''}, which the app's host compiles in`,
|
|
358
|
+
));
|
|
359
|
+
return [...packageRows, ...carried];
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
309
363
|
function appRuntimeRows(platform) {
|
|
310
364
|
const hostRoot = resolveAppHostRoot();
|
|
311
365
|
const hermesLicense = textOf(path.join(hostRoot, 'third_party', 'jsi', 'LICENSE'));
|
|
@@ -347,24 +401,9 @@ function appRuntimeRows(platform) {
|
|
|
347
401
|
nativeVersion: WEBVIEW.version,
|
|
348
402
|
sha256: WEBVIEW.sha256,
|
|
349
403
|
sourceUrl: 'https://github.com/webview/webview',
|
|
350
|
-
licenseNotes: 'webview/webview, with the local
|
|
404
|
+
licenseNotes: 'webview/webview, with the local patches @crossbind/core-app-host lists',
|
|
351
405
|
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
|
|
352
406
|
}]),
|
|
353
|
-
// A desktop host checks its updates with Monocypher, which the linker leaves out of one without app.updater.
|
|
354
|
-
...(['ios', 'android'].includes(platform) || state.config.app?.updater === undefined ? [] : [{
|
|
355
|
-
npmName: '@crossbind/core-app-host',
|
|
356
|
-
name: 'monocypher',
|
|
357
|
-
version: MONOCYPHER.version,
|
|
358
|
-
nativeVersion: MONOCYPHER.version,
|
|
359
|
-
sha256: MONOCYPHER.sha256,
|
|
360
|
-
license: 'BSD-2-Clause OR CC0-1.0',
|
|
361
|
-
licenseDeclared: 'BSD-2-Clause OR CC0-1.0',
|
|
362
|
-
licenseSelected: null,
|
|
363
|
-
isCopyleft: false,
|
|
364
|
-
sourceUrl: `https://monocypher.org/download/monocypher-${MONOCYPHER.version}.tar.gz`,
|
|
365
|
-
licenseNotes: "Monocypher's Ed25519 and SHA-512, compiled into the host, which check the app's updates",
|
|
366
|
-
licenseText: textOf(path.join(hostRoot, 'third_party', 'monocypher', 'LICENCE.md')),
|
|
367
|
-
}]),
|
|
368
407
|
];
|
|
369
408
|
}
|
|
370
409
|
|
|
@@ -484,6 +523,16 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
|
|
|
484
523
|
if (platform === 'wasi') rows.push(...wasiToolchainRows());
|
|
485
524
|
if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
|
|
486
525
|
if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
|
|
487
|
-
if (runtimeEnv === 'app')
|
|
526
|
+
if (runtimeEnv === 'app') {
|
|
527
|
+
rows.push(...appRuntimeRows(platform));
|
|
528
|
+
// A library two extensions carry, or one the app has already, is listed once for each version the app ships.
|
|
529
|
+
const versionOf = (row) => `${row.name}@${row.nativeVersion ?? row.version}`;
|
|
530
|
+
const listed = new Set(rows.map(versionOf));
|
|
531
|
+
for (const row of [...extensionRows(platform), ...installerRows(platform)]) {
|
|
532
|
+
if (listed.has(versionOf(row))) continue;
|
|
533
|
+
listed.add(versionOf(row));
|
|
534
|
+
rows.push(row);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
488
537
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
489
538
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { downloadFile, verifyIntegrity } from '../utils/downloadAndExtractFile.js';
|
|
4
|
+
|
|
5
|
+
// The Ubuntu 22.04 packages a Linux extension lists beside crossbind's sysroot: each downloaded once into the build's
|
|
6
|
+
// folder from Launchpad, as the sysroot's own are, checked against its SHA-256 and unpacked into a sysroot of the
|
|
7
|
+
// extension's own for the target, whose headers its sources include and whose libraries the host links against. Its
|
|
8
|
+
// users' systems load the libraries at run time, as the app's Debian package's Depends asks for them.
|
|
9
|
+
|
|
10
|
+
const DEBIAN_ARCH = { x64: 'amd64', arm64: 'arm64' };
|
|
11
|
+
const MULTIARCH = { x64: 'x86_64-linux-gnu', arm64: 'aarch64-linux-gnu' };
|
|
12
|
+
|
|
13
|
+
export const packageUrl = (file) => `https://launchpad.net/ubuntu/+archive/primary/+files/${file}`;
|
|
14
|
+
|
|
15
|
+
// The packages of the target's architecture among the lines an extension lists.
|
|
16
|
+
export function packagesFor(target, lines) {
|
|
17
|
+
return lines.map((line) => line.split(' ')).filter(([arch]) => arch === DEBIAN_ARCH[target.arch]).map(([, file, sha256]) => ({ file, sha256 }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// What a sysroot gives the target: the folders its sources include, usr/include and each folder in it, as a library's
|
|
21
|
+
// headers sit in one of its own, and the folder the host links from.
|
|
22
|
+
export function sysrootDirs(target, root) {
|
|
23
|
+
const include = path.join(root, 'usr', 'include');
|
|
24
|
+
const nested = fs.existsSync(include)
|
|
25
|
+
? fs.readdirSync(include, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(include, entry.name))
|
|
26
|
+
: [];
|
|
27
|
+
return { includes: [include, ...nested], libDir: path.join(root, 'usr', 'lib', MULTIARCH[target.arch]) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Unpacks each extension's packages for the target, with unpack(deb, folder), which crossbind's Linux image runs.
|
|
31
|
+
export async function unpackPackages(target, compiled, { cache, workDir, unpack }) {
|
|
32
|
+
const sysroots = [];
|
|
33
|
+
for (const { name, packages } of compiled.filter((part) => part.packages.length > 0)) {
|
|
34
|
+
const own = packagesFor(target, packages);
|
|
35
|
+
if (own.length === 0) throw new Error(`crossbind: the extension ${name} lists Linux packages, but none of ${DEBIAN_ARCH[target.arch]}, which an ${target.arch} app takes.`);
|
|
36
|
+
const root = path.join(workDir, 'sysroot', name);
|
|
37
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
38
|
+
fs.mkdirSync(root, { recursive: true });
|
|
39
|
+
for (const { file, sha256 } of own) {
|
|
40
|
+
const url = packageUrl(file);
|
|
41
|
+
const deb = await downloadFile(url, cache);
|
|
42
|
+
verifyIntegrity(deb, url, sha256);
|
|
43
|
+
unpack(deb, root);
|
|
44
|
+
}
|
|
45
|
+
sysroots.push({ name, ...sysrootDirs(target, root) });
|
|
46
|
+
}
|
|
47
|
+
return sysroots;
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The verification metadata of an Android app whose extensions ask Maven for libraries: the android-app image's, which
|
|
2
|
+
// lists all crossbind's own project resolves, with each extension's components, as Gradle's
|
|
3
|
+
// --write-verification-metadata lists them. Gradle then checks every file it resolves, from the image's cache or online.
|
|
4
|
+
|
|
5
|
+
const COMPONENT = /<component group="([^"]+)" name="([^"]+)" version="([^"]+)"[^>]*>([\s\S]*?)<\/component>/g;
|
|
6
|
+
const ARTIFACT = /<artifact name="([^"]+)">[\s\S]*?<\/artifact>/g;
|
|
7
|
+
// What would leave a file unchecked, which an extension's metadata may not add to the app's.
|
|
8
|
+
const TRUST = /<(trusted-artifacts|trust|ignored-keys|trusted-keys)\b/;
|
|
9
|
+
|
|
10
|
+
export function mergedVerification(base, fragments) {
|
|
11
|
+
const components = new Map();
|
|
12
|
+
const add = (xml, owner) => {
|
|
13
|
+
for (const [, group, name, version, body] of xml.matchAll(COMPONENT)) {
|
|
14
|
+
const key = `${group}:${name}:${version}`;
|
|
15
|
+
const artifacts = components.get(key) ?? new Map();
|
|
16
|
+
for (const [block, artifact] of body.matchAll(ARTIFACT)) {
|
|
17
|
+
const known = artifacts.get(artifact);
|
|
18
|
+
if (known && known.block !== block) throw new Error(`crossbind: ${owner} gives ${artifact} of ${key} other checksums than ${known.owner} does.`);
|
|
19
|
+
if (!known) artifacts.set(artifact, { block, owner });
|
|
20
|
+
}
|
|
21
|
+
components.set(key, artifacts);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
add(base, "the android-app image's verification metadata");
|
|
25
|
+
for (const { name, xml } of fragments) {
|
|
26
|
+
const owner = `the extension ${name}`;
|
|
27
|
+
if (TRUST.test(xml)) throw new Error(`crossbind: ${owner}'s verification metadata trusts files Gradle would then leave unchecked, which no extension's may.`);
|
|
28
|
+
if ([...xml.matchAll(COMPONENT)].length === 0) throw new Error(`crossbind: ${owner}'s verification metadata lists no component, as Gradle's --write-verification-metadata sha256 writes them.`);
|
|
29
|
+
add(xml, owner);
|
|
30
|
+
}
|
|
31
|
+
const listed = [...components.entries()].sort(([one], [other]) => (one < other ? -1 : 1)).map(([key, artifacts]) => {
|
|
32
|
+
const [group, name, version] = key.split(':');
|
|
33
|
+
const blocks = [...artifacts.values()].map(({ block }) => ` ${block}\n`).join('');
|
|
34
|
+
return ` <component group="${group}" name="${name}" version="${version}">\n${blocks} </component>\n`;
|
|
35
|
+
});
|
|
36
|
+
return `${base.slice(0, base.indexOf('<components>'))}<components>\n${listed.join('')} </components>\n</verification-metadata>\n`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import state from '../state/index.js';
|
|
2
|
+
import loadConfig from '../state/loadConfig.js';
|
|
3
|
+
import {
|
|
4
|
+
DESKTOP_PLATFORMS, MAX_VERSION_CODE, appOutputOf, clearAppOutputs,
|
|
5
|
+
} from './buildApp.js';
|
|
6
|
+
import extensionApi from './extensionApi.js';
|
|
7
|
+
import {
|
|
8
|
+
PUBLISH_PLATFORMS, extensionStep, isJsonValue, ownExtensions, withListed, withoutNamed,
|
|
9
|
+
} from '../utils/appExtensions.js';
|
|
10
|
+
import { CONFIG_CHANGES } from '../utils/configChanges.js';
|
|
11
|
+
import { hasTerminal } from '../utils/ask.js';
|
|
12
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
13
|
+
|
|
14
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
const possessive = (name) => (name.endsWith('s') ? `${name}'` : `${name}'s`);
|
|
16
|
+
const listed = (names, word = 'or') => (names.length > 1 ? `${names.slice(0, -1).join(', ')} ${word} ${names.at(-1)}` : names[0]);
|
|
17
|
+
const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
18
|
+
const camel = (name) => name.replace(/-([a-z0-9])/g, (dash, letter) => letter.toUpperCase());
|
|
19
|
+
const storeName = ({ name, publisher }) => publisher.store ?? name;
|
|
20
|
+
// How many builds a publish makes at most, as its first publisher may ask for another.
|
|
21
|
+
const MAX_BUILDS = 4;
|
|
22
|
+
|
|
23
|
+
// A step of the publisher's, which its publisher may lack.
|
|
24
|
+
const step = ({ name, publisher }, which, ...args) => extensionStep(name, which, () => publisher[which]?.(...args));
|
|
25
|
+
|
|
26
|
+
// crossbind's own publishers, which the host brings as it brings its other extensions.
|
|
27
|
+
export const builtInPublishers = () => withoutNamed(ownExtensions(resolveAppHostRoot()), state.config?.app).filter(({ publisher }) => publisher);
|
|
28
|
+
|
|
29
|
+
// The publishers an app has: crossbind's own, each in the place of the app's extension of its name that has a
|
|
30
|
+
// publisher, then those of the app's other extensions.
|
|
31
|
+
export const publishersOf = (extensions) => withListed(builtInPublishers(), extensions.filter((extension) => extension.publisher));
|
|
32
|
+
|
|
33
|
+
// The store the command names first, before the options, else the last publisher that says it is the one of the platform
|
|
34
|
+
// -p, or CROSSBIND_APP_PLATFORM, names, and the arguments after it. A store named after the options, of those crossbind
|
|
35
|
+
// knows, is refused, as Vite would take it.
|
|
36
|
+
export function storeOf(args, platform, envPlatform, publishers = []) {
|
|
37
|
+
const [first, ...rest] = args;
|
|
38
|
+
const named = first !== undefined && !first.startsWith('-') ? first : null;
|
|
39
|
+
const late = named ? null : args.find((arg) => publishers.some(({ name }) => name === arg));
|
|
40
|
+
if (late) throw new Error(`crossbind: crossbind app publish takes the store first, before the options: crossbind app publish ${late} ${args.filter((arg) => arg !== late).join(' ')}.`);
|
|
41
|
+
const where = platform ?? envPlatform;
|
|
42
|
+
const usual = publishers.findLast(({ publisher }) => publisher.isDefault && publisher.platforms.includes(where));
|
|
43
|
+
return { store: named ?? usual?.name ?? null, rest: named ? rest : args };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The publisher of a store, after those it comes after, first to last.
|
|
47
|
+
export function publishChain(publishers, store) {
|
|
48
|
+
if (!publishers.some(({ name }) => name === store)) {
|
|
49
|
+
throw new Error(`crossbind: crossbind app publish takes the store it sends the app to first: ${listed(publishers.map(({ name }) => name))}${store ? `, not ${store}` : ''}.`);
|
|
50
|
+
}
|
|
51
|
+
const chain = [];
|
|
52
|
+
for (let current = store; current;) {
|
|
53
|
+
const extension = publishers.find(({ name }) => name === current);
|
|
54
|
+
if (!extension) throw new Error(`crossbind: ${chain[0].name} comes after ${current}, which no publisher is named.`);
|
|
55
|
+
if (chain.includes(extension)) throw new Error(`crossbind: ${listed([extension, ...chain].map(({ name }) => name), 'and')} come after one another in a circle.`);
|
|
56
|
+
chain.unshift(extension);
|
|
57
|
+
current = extension.publisher.after;
|
|
58
|
+
}
|
|
59
|
+
return chain;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The platform a publish builds for: the one -p names, which every publisher of it takes, else the only one they take,
|
|
63
|
+
// else CROSSBIND_APP_PLATFORM's, else this machine's desktop one.
|
|
64
|
+
export function publishPlatform(chain, platform, envPlatform, host = process.platform) {
|
|
65
|
+
const store = chain.at(-1).name;
|
|
66
|
+
const platforms = chain.reduce((taken, { publisher }) => taken.filter((one) => publisher.platforms.includes(one)), PUBLISH_PLATFORMS);
|
|
67
|
+
if (platforms.length === 0) throw new Error(`crossbind: ${store} comes after ${chain[0].name}, which takes apps of none of its platforms.`);
|
|
68
|
+
const takes = `${store} takes ${listed(platforms)} apps`;
|
|
69
|
+
if (platform) {
|
|
70
|
+
if (!platforms.includes(platform)) throw new Error(`crossbind: ${takes}, not ${possessive(platform)}, which -p names.`);
|
|
71
|
+
return platform;
|
|
72
|
+
}
|
|
73
|
+
if (platforms.length === 1) return platforms[0];
|
|
74
|
+
if (envPlatform) {
|
|
75
|
+
if (!platforms.includes(envPlatform)) throw new Error(`crossbind: ${takes}, not ${possessive(envPlatform)}, which CROSSBIND_APP_PLATFORM names.`);
|
|
76
|
+
return envPlatform;
|
|
77
|
+
}
|
|
78
|
+
if (platforms.includes(DESKTOP_PLATFORMS[host])) return DESKTOP_PLATFORMS[host];
|
|
79
|
+
throw new Error(`crossbind: ${takes}: name one with -p.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The options of each publisher of a publish, as the command line gives them, and the arguments Vite takes. An option
|
|
83
|
+
// that another publisher takes is refused, naming whose it is; any other goes on to Vite.
|
|
84
|
+
export function publishOptions(chain, args, publishers) {
|
|
85
|
+
const kinds = new Map();
|
|
86
|
+
chain.forEach(({ name, publisher }) => Object.entries(publisher.options).forEach(([option, { value }]) => {
|
|
87
|
+
const first = kinds.get(option);
|
|
88
|
+
if (first && (first.value === null) !== (value === null)) {
|
|
89
|
+
throw new Error(`crossbind: ${first.name} and ${name} both take --${kebab(option)}, one as a flag and one with a value: one of them names it otherwise.`);
|
|
90
|
+
}
|
|
91
|
+
if (!first) kinds.set(option, { name, value });
|
|
92
|
+
}));
|
|
93
|
+
const given = new Map(chain.map(({ name }) => [name, new Map()]));
|
|
94
|
+
const foreign = new Map();
|
|
95
|
+
const viteArgs = [];
|
|
96
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
97
|
+
const match = /^--([a-z][a-z0-9-]*)(?:=(.*))?$/s.exec(args[index]);
|
|
98
|
+
const option = match && camel(match[1]);
|
|
99
|
+
const owners = match ? chain.filter(({ publisher }) => Object.hasOwn(publisher.options, option)) : [];
|
|
100
|
+
if (owners.length === 0) {
|
|
101
|
+
const other = match && publishers.find(({ publisher }) => Object.hasOwn(publisher.options, option));
|
|
102
|
+
if (other) foreign.set(other.name, [...(foreign.get(other.name) ?? []), `--${match[1]}`]);
|
|
103
|
+
else viteArgs.push(args[index]);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const { value: placeholder } = owners.at(-1).publisher.options[option];
|
|
107
|
+
let value = true;
|
|
108
|
+
if (placeholder !== null && match[2] === undefined) {
|
|
109
|
+
value = args[index + 1];
|
|
110
|
+
index += 1;
|
|
111
|
+
if (value === undefined || value.startsWith('-')) throw new Error(`crossbind: --${match[1]} takes a value, as ${placeholder}.`);
|
|
112
|
+
} else if (placeholder !== null) {
|
|
113
|
+
value = match[2];
|
|
114
|
+
} else if (match[2] !== undefined) {
|
|
115
|
+
throw new Error(`crossbind: --${match[1]} takes no value.`);
|
|
116
|
+
}
|
|
117
|
+
owners.forEach(({ name }) => given.get(name).set(option, value));
|
|
118
|
+
}
|
|
119
|
+
if (foreign.size > 0) {
|
|
120
|
+
const whose = [...foreign].map(([owner, names]) => `${listed(names, 'and')} ${names.length > 1 ? 'are' : 'is'} ${possessive(owner)}`);
|
|
121
|
+
throw new Error(`crossbind: ${listed(whose, 'and')}, which ${chain.at(-1).name} takes none of.`);
|
|
122
|
+
}
|
|
123
|
+
const options = Object.fromEntries(chain.map(({ name, publisher }) => [name, Object.fromEntries(Object.entries(publisher.options)
|
|
124
|
+
.map(([option, { value, default: fallback }]) => [option, given.get(name).get(option) ?? (value === null ? false : fallback)]))]));
|
|
125
|
+
return { options, viteArgs };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The help of the stores crossbind publishes to itself, with the options each takes after its name.
|
|
129
|
+
export function publishHelp(publishers = builtInPublishers()) {
|
|
130
|
+
const signature = (option, { value }) => `--${kebab(option)}${value ? ` ${value}` : ''}`;
|
|
131
|
+
const width = Math.max(...publishers.flatMap(({ publisher }) => Object.entries(publisher.options).map(([option, declared]) => signature(option, declared).length)));
|
|
132
|
+
return [
|
|
133
|
+
'',
|
|
134
|
+
'Stores, each with the options it takes after its name:',
|
|
135
|
+
...publishers.flatMap(({ name, publisher }) => [
|
|
136
|
+
` ${name}: ${storeName({ name, publisher })}, for ${listed(publisher.platforms)} apps${publisher.isDefault ? ', which goes first for them' : ''}`,
|
|
137
|
+
...Object.entries(publisher.options).map(([option, declared]) => ` ${signature(option, declared).padEnd(width)} ${declared.help}${declared.default ? ` (default: ${declared.default})` : ''}`),
|
|
138
|
+
]),
|
|
139
|
+
"An app's extensions add stores of their own, each with the options its package gives.",
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The changes the first publisher makes to the config for its build: a JSON merge patch of crossbind.config.js's
|
|
144
|
+
// export.
|
|
145
|
+
async function buildChanges(extension, api) {
|
|
146
|
+
const { name } = extension;
|
|
147
|
+
const changes = await step(extension, 'build', api);
|
|
148
|
+
if (changes === null || changes === undefined) return null;
|
|
149
|
+
if (!isObject(changes) || !isJsonValue(changes)) {
|
|
150
|
+
throw new Error(`crossbind: ${name}'s build gives no changes to crossbind.config.js, which are an object of JSON values merged into its export, where null removes a key.`);
|
|
151
|
+
}
|
|
152
|
+
return changes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Hands the build's output to the uploads of the publishers after the first, each with what the one before it put out.
|
|
156
|
+
async function uploadAfter(chain, apis, sessions, output, firstPublished) {
|
|
157
|
+
let published = firstPublished;
|
|
158
|
+
for (const [index, extension] of chain.entries()) {
|
|
159
|
+
const { name } = extension;
|
|
160
|
+
if (index > 0) {
|
|
161
|
+
const answer = await step(extension, 'upload', apis[index], sessions[index], output, published);
|
|
162
|
+
if (answer?.isRebuildAsked) throw new Error(`crossbind: ${name} asks for another build, which ${chain[0].name}, whose build it publishes from, alone can ask for.`);
|
|
163
|
+
published = answer?.published;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Sends an app to a store through its publisher, after those it comes after. All they need is checked before the build,
|
|
169
|
+
// which takes minutes: on a terminal they ask for what is missing and set it up; without one, as in CI, every missing
|
|
170
|
+
// piece is named at once. crossbind then builds the app once, with the first publisher's changes to the config, build
|
|
171
|
+
// number and environment, and hands its output to each one's upload in turn. The first may ask for another build, as
|
|
172
|
+
// an iOS app's does once its user has given Apple's reasons for an API it calls.
|
|
173
|
+
export default async function publish(args, { platform, envPlatform, runVite }) {
|
|
174
|
+
const publishers = publishersOf(state.config.extensions);
|
|
175
|
+
const { store, rest } = storeOf(args, platform, envPlatform, publishers);
|
|
176
|
+
const chain = publishChain(publishers, store);
|
|
177
|
+
const own = builtInPublishers().map(({ name }) => name);
|
|
178
|
+
chain.filter((extension) => own.includes(extension.name) && state.config.extensions.includes(extension))
|
|
179
|
+
.forEach(({ name, root }) => console.log(`crossbind: the extension ${name} of ${root} replaces crossbind's own publisher of its name.`));
|
|
180
|
+
const target = publishPlatform(chain, platform, envPlatform);
|
|
181
|
+
const { options, viteArgs } = publishOptions(chain, rest, publishers);
|
|
182
|
+
const run = { buildNumber: undefined };
|
|
183
|
+
const isGuided = hasTerminal();
|
|
184
|
+
const apis = chain.map(({ name }) => extensionApi(name, {
|
|
185
|
+
platform: target, options: options[name], isGuided, run,
|
|
186
|
+
}));
|
|
187
|
+
const [first] = chain;
|
|
188
|
+
const changes = await buildChanges(first, apis[0]);
|
|
189
|
+
if (changes) state.config = await loadConfig(undefined, undefined, { changes });
|
|
190
|
+
const checked = [];
|
|
191
|
+
for (const [index, extension] of chain.entries()) checked.push((await step(extension, 'check', apis[index])) ?? {});
|
|
192
|
+
const missing = checked.flatMap((result) => result.missing ?? []);
|
|
193
|
+
if (missing.length > 0) throw new Error(missing.join('\n'));
|
|
194
|
+
const sessions = checked.map(({ session }) => session);
|
|
195
|
+
run.buildNumber = (await step(first, 'buildNumber', apis[0], sessions[0])) ?? null;
|
|
196
|
+
if (run.buildNumber !== null && !(Number.isInteger(run.buildNumber) && run.buildNumber >= 1 && run.buildNumber <= MAX_VERSION_CODE)) {
|
|
197
|
+
throw new Error(`crossbind: ${first.name}'s buildNumber gives ${JSON.stringify(run.buildNumber)}, which is no build number: a whole number from 1 to ${MAX_VERSION_CODE}, as both stores take, or null for the app's own.`);
|
|
198
|
+
}
|
|
199
|
+
for (let builds = 1; ; builds += 1) {
|
|
200
|
+
clearAppOutputs();
|
|
201
|
+
const buildEnv = await step(first, 'buildEnv', apis[0], sessions[0]);
|
|
202
|
+
const exitCode = await runVite(['build', ...viteArgs], {
|
|
203
|
+
...process.env,
|
|
204
|
+
...(changes ? { [CONFIG_CHANGES]: JSON.stringify(changes) } : {}),
|
|
205
|
+
...buildEnv,
|
|
206
|
+
CROSSBIND_APP_PLATFORM: target,
|
|
207
|
+
...(run.buildNumber === null ? {} : { CROSSBIND_APP_BUILD_NUMBER: String(run.buildNumber) }),
|
|
208
|
+
});
|
|
209
|
+
if (exitCode !== 0) {
|
|
210
|
+
throw Object.assign(new Error(`crossbind: the build ended with exit code ${exitCode}, so nothing went to ${storeName(first)}.`), { exitCode });
|
|
211
|
+
}
|
|
212
|
+
const output = appOutputOf(target);
|
|
213
|
+
const answer = await step(first, 'upload', apis[0], sessions[0], output);
|
|
214
|
+
if (!answer?.isRebuildAsked) {
|
|
215
|
+
await uploadAfter(chain, apis, sessions, output, answer?.published);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (builds === MAX_BUILDS) throw new Error(`crossbind: ${first.name} asks for another build after ${MAX_BUILDS}, as many as a publish makes.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -49,7 +49,7 @@ export function windowsSigning(env = process.env) {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
if (TOKENS.includes(storetype)) {
|
|
52
|
-
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is a hardware token, which crossbind's windows container does not reach. Sign with a cloud's key service, as Azure
|
|
52
|
+
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is a hardware token, which crossbind's windows container does not reach. Sign with a cloud's key service, as Azure Artifact Signing (TRUSTEDSIGNING) or Azure Key Vault (AZUREKEYVAULT), or with a PKCS#12 file.`);
|
|
53
53
|
}
|
|
54
54
|
if (![...FILE_STORES, ...CLOUDS].includes(storetype)) {
|
|
55
55
|
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is no store type jsign takes: ${[...FILE_STORES, ...CLOUDS].join(', ')}.`);
|