crossbind 2.0.0-beta.65 → 2.0.0-beta.67
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/package.json +6 -1
- package/src/actions/androidDevice.js +1 -1
- package/src/actions/appInstaller.js +177 -0
- package/src/actions/appUpdate.js +68 -0
- package/src/actions/appleSigning.js +96 -0
- package/src/actions/buildApp.js +110 -32
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildExternal.js +8 -1
- package/src/actions/createLib.js +10 -5
- package/src/actions/getCmakeParameters.js +4 -1
- package/src/actions/gradleProject.js +65 -4
- package/src/actions/licenses.js +31 -2
- package/src/actions/windowsSigning.js +96 -0
- package/src/actions/xcodeProject.js +44 -10
- package/src/app/index.d.ts +171 -0
- package/src/app/index.js +245 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +9 -0
- package/src/index.js +2 -1
- package/src/state/loadConfig.js +1 -1
- package/src/utils/appApiHeader.js +29 -0
- package/src/utils/appIcon.js +174 -0
- package/src/utils/appOptions.js +321 -0
- package/src/utils/embindArgumentGuards.js +8 -5
- package/src/utils/embindCallArguments.js +6 -6
- package/src/utils/overrideDependency.js +3 -0
- package/src/utils/recipePatches.js +61 -0
- package/src/utils/sourceFingerprint.js +8 -1
- package/src/utils/writeIfChanged.js +1 -1
- package/vitest.config.js +14 -0
|
@@ -7,6 +7,7 @@ import state from '../state/index.js';
|
|
|
7
7
|
import buildLib from './buildLib.js';
|
|
8
8
|
import buildBinTools from './buildBinTools.js';
|
|
9
9
|
import { copyBundledLicenses } from '../utils/bundledLicenses.js';
|
|
10
|
+
import { applyRecipePatches, refreshPatchedSource, writePatchedSourceStamp } from '../utils/recipePatches.js';
|
|
10
11
|
|
|
11
12
|
export default async function buildExternal(targetParams, options = {}) {
|
|
12
13
|
const version = state.config.package.nativeVersion;
|
|
@@ -14,9 +15,15 @@ export default async function buildExternal(targetParams, options = {}) {
|
|
|
14
15
|
throw new Error(`crossbind: "${state.config.general?.name}" has a crossbind.build recipe but no "nativeVersion" in package.json — cannot build from source.`);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const { sha256, replaceList, copyToSource, copyToDist } = state.config.build;
|
|
18
|
+
const { sha256, replaceList, copyToSource, copyToDist, patches } = state.config.build;
|
|
19
|
+
refreshPatchedSource(state.config.paths.build, patches);
|
|
18
20
|
const isNewlyCreated = await downloadAndExtractFile(sourcesOf(state.config.build, version), state.config.paths.build, sha256);
|
|
19
21
|
const sourcePath = `${state.config.paths.build}/source`;
|
|
22
|
+
// Upstream fixes are diffs against the source as released, so they go in before crossbind's own edits.
|
|
23
|
+
if (isNewlyCreated) {
|
|
24
|
+
applyRecipePatches(sourcePath, patches);
|
|
25
|
+
writePatchedSourceStamp(state.config.paths.build, patches);
|
|
26
|
+
}
|
|
20
27
|
if (isNewlyCreated && replaceList) {
|
|
21
28
|
replaceList.forEach(({ regex, replacement, paths }) => {
|
|
22
29
|
replace({
|
package/src/actions/createLib.js
CHANGED
|
@@ -11,6 +11,7 @@ import state from '../state/index.js';
|
|
|
11
11
|
import logger from '../utils/logger.js';
|
|
12
12
|
import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
|
|
13
13
|
import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
|
|
14
|
+
import { appApiHeader, compilesAsApp, stageAppApiInclude } from '../utils/appApiHeader.js';
|
|
14
15
|
import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
|
|
15
16
|
import { withDependencyBridges } from '../utils/dependencyBridges.js';
|
|
16
17
|
import { targetArchiveFlags } from '../utils/archiveFlags.js';
|
|
@@ -65,7 +66,9 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
65
66
|
: null;
|
|
66
67
|
// An Android app links the project's archives from the build where React Native's shared library comes from.
|
|
67
68
|
const appArchives = target.platform === 'android' && target.runtimeEnv === 'app' ? 'archives' : null;
|
|
68
|
-
|
|
69
|
+
// An app's own code compiles against <crossbind/app.h>, with CROSSBIND_APP defined.
|
|
70
|
+
const appApi = compilesAsApp(target) ? getFilesFingerprint([appApiHeader()]) : null;
|
|
71
|
+
const extraKeys = [httpHeader, appArchives, appApi].filter(Boolean);
|
|
69
72
|
const flagsFingerprint = getContentHash(JSON.stringify(extraKeys.length ? [keyInputs, ...extraKeys] : keyInputs));
|
|
70
73
|
const flagsChanged = !fs.existsSync(flagsFingerprintFile)
|
|
71
74
|
|| fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
|
|
@@ -152,10 +155,12 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
152
155
|
buildEnv.params.push('-e', e);
|
|
153
156
|
});
|
|
154
157
|
} else {
|
|
155
|
-
// The source of an http build includes <crossbind/http.h>.
|
|
156
|
-
const headerDirs =
|
|
157
|
-
|
|
158
|
-
:
|
|
158
|
+
// The source of an http build includes <crossbind/http.h>, and an app's own code <crossbind/app.h>.
|
|
159
|
+
const headerDirs = [
|
|
160
|
+
...(options.headerDirs ?? []),
|
|
161
|
+
...(target.platform === 'wasi' ? [stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)] : []),
|
|
162
|
+
...(compilesAsApp(target) ? [stageAppApiInclude(`${buildPath}/crossbind-app`)] : []),
|
|
163
|
+
];
|
|
159
164
|
buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
|
|
160
165
|
|
|
161
166
|
triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
|
|
@@ -1,6 +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
5
|
|
|
5
6
|
export default function getCmakeParameters(target, options = {}) {
|
|
6
7
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
@@ -37,7 +38,9 @@ export default function getCmakeParameters(target, options = {}) {
|
|
|
37
38
|
const buildType = sharedPlatforms.includes(target.platform) && target.runtimeEnv !== 'app' ? 'SHARED' : 'STATIC';
|
|
38
39
|
const otherBuildType = buildType === 'STATIC' ? 'SHARED' : 'STATIC';
|
|
39
40
|
|
|
40
|
-
|
|
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])];
|
|
41
44
|
|
|
42
45
|
params.push(...[
|
|
43
46
|
`-DPROJECT_NAME=${options.name || state.config.general.name}`,
|
|
@@ -14,7 +14,50 @@ export const ANDROIDX_WEBKIT = '1.17.1';
|
|
|
14
14
|
|
|
15
15
|
const groovyString = (text) => `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// The launcher shapes an adaptive icon: the app's icon in the middle of a white square.
|
|
18
|
+
const ADAPTIVE_ICON = `<?xml version="1.0" encoding="utf-8"?>
|
|
19
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
20
|
+
<background android:drawable="@android:color/white" />
|
|
21
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
|
22
|
+
</adaptive-icon>
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const DAY_NIGHT = '@android:style/Theme.DeviceDefault.DayNight';
|
|
26
|
+
// The activity's theme shows app.window.backgroundColor until the page paints, and the activity puts it behind the page,
|
|
27
|
+
// whose WebView would be white; transparent, which the activity takes for none, without it.
|
|
28
|
+
const backgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
|
|
29
|
+
<resources>
|
|
30
|
+
<color name="crossbind_background">${color ?? '#00000000'}</color>
|
|
31
|
+
<style name="CrossbindBackground" parent="${DAY_NIGHT}">
|
|
32
|
+
<item name="android:windowBackground">@color/crossbind_background</item>
|
|
33
|
+
</style>
|
|
34
|
+
</resources>
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
// The Android permissions behind each of app.permissions, after the one every app asks for; the activity asks its user
|
|
38
|
+
// for them when the page first uses the device.
|
|
39
|
+
const INTERNET = ' <uses-permission android:name="android.permission.INTERNET" />';
|
|
40
|
+
const ANDROID_PERMISSIONS = {
|
|
41
|
+
camera: ['CAMERA'],
|
|
42
|
+
microphone: ['RECORD_AUDIO', 'MODIFY_AUDIO_SETTINGS'],
|
|
43
|
+
location: ['ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION'],
|
|
44
|
+
};
|
|
45
|
+
const usesPermissions = (permissions) => permissions
|
|
46
|
+
.flatMap((name) => ANDROID_PERMISSIONS[name])
|
|
47
|
+
.map((permission) => ` <uses-permission android:name="android.permission.${permission}" />`);
|
|
48
|
+
|
|
49
|
+
// The links of app.urlSchemes the activity opens, from a browser as well, before the end of the activity's element.
|
|
50
|
+
const ACTIVITY_END = ' </activity>';
|
|
51
|
+
const urlFilter = (schemes) => [
|
|
52
|
+
' <intent-filter>',
|
|
53
|
+
' <action android:name="android.intent.action.VIEW" />',
|
|
54
|
+
' <category android:name="android.intent.category.DEFAULT" />',
|
|
55
|
+
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
56
|
+
...schemes.map((scheme) => ` <data android:scheme="${scheme}" />`),
|
|
57
|
+
' </intent-filter>',
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
const buildGradle = ({ id, label, version, abis, icon, launchMode, theme }) => `// crossbind writes this project anew on every build: the module comes prebuilt in src/main/jniLibs/, and the app's
|
|
18
61
|
// files are in src/main/assets/app/.
|
|
19
62
|
plugins {
|
|
20
63
|
id 'com.android.application'
|
|
@@ -31,7 +74,7 @@ android {
|
|
|
31
74
|
targetSdk ${TARGET_SDK}
|
|
32
75
|
versionCode ${version.code}
|
|
33
76
|
versionName ${groovyString(version.name)}
|
|
34
|
-
manifestPlaceholders = [appName: ${groovyString(label)}]
|
|
77
|
+
manifestPlaceholders = [appName: ${groovyString(label)}, appIcon: ${groovyString(icon)}, launchMode: ${groovyString(launchMode)}, appTheme: ${groovyString(theme)}]
|
|
35
78
|
ndk {
|
|
36
79
|
abiFilters ${abis.map(groovyString).join(', ')}
|
|
37
80
|
}
|
|
@@ -95,16 +138,34 @@ dependencies {
|
|
|
95
138
|
|
|
96
139
|
// An Android app as a Gradle project, written anew from the parts its ABI targets built: each brings its libraries,
|
|
97
140
|
// the first also the app's files. Gradle adds Hermes and androidx.webkit from Maven, and makes the APK or the bundle.
|
|
98
|
-
|
|
141
|
+
// The icons given are the files of the app's launcher icon under res/; without them it has the system's.
|
|
142
|
+
export default function writeGradleProject(to, { parts, id, label, version, icons = null, urlSchemes = [], background = null, permissions = [] }) {
|
|
99
143
|
const { name } = state.config.general;
|
|
100
144
|
if (parts.length === 0) throw new Error(`crossbind: no part of the Android app ${name} is built, so there is no Gradle project to write.`);
|
|
101
145
|
fs.rmSync(to, { recursive: true, force: true });
|
|
102
146
|
fs.cpSync(path.join(resolveAppHostRoot(), 'android'), to, { recursive: true });
|
|
103
147
|
const main = path.join(to, 'app', 'src', 'main');
|
|
148
|
+
const manifest = path.join(main, 'AndroidManifest.xml');
|
|
149
|
+
const text = fs.readFileSync(manifest, 'utf8');
|
|
150
|
+
if (!text.includes(ACTIVITY_END) || !text.includes(INTERNET)) {
|
|
151
|
+
throw new Error(`crossbind: ${manifest}, from the app host, has no activity or permissions to give app.urlSchemes and app.permissions: update @crossbind/core-app-host to match crossbind.`);
|
|
152
|
+
}
|
|
153
|
+
fs.writeFileSync(manifest, text
|
|
154
|
+
.replace(INTERNET, [INTERNET, ...usesPermissions(permissions)].join('\n'))
|
|
155
|
+
.replace(ACTIVITY_END, urlSchemes.length > 0 ? `${urlFilter(urlSchemes)}\n${ACTIVITY_END}` : ACTIVITY_END));
|
|
104
156
|
parts.forEach(({ arch, dir }) => fs.cpSync(path.join(dir, 'lib'), path.join(main, 'jniLibs', arch), { recursive: true }));
|
|
105
157
|
fs.cpSync(path.join(parts[0].dir, 'app'), path.join(main, 'assets', 'app'), { recursive: true });
|
|
158
|
+
if (icons) {
|
|
159
|
+
Object.entries({ ...icons, 'mipmap-anydpi/ic_launcher.xml': ADAPTIVE_ICON }).forEach(([file, content]) => {
|
|
160
|
+
fs.mkdirSync(path.dirname(path.join(main, 'res', file)), { recursive: true });
|
|
161
|
+
fs.writeFileSync(path.join(main, 'res', file), content);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(path.join(main, 'res', 'values'), { recursive: true });
|
|
165
|
+
fs.writeFileSync(path.join(main, 'res', 'values', 'background.xml'), backgroundValues(background));
|
|
106
166
|
fs.writeFileSync(path.join(to, 'app', 'build.gradle'), buildGradle({
|
|
107
|
-
id, label
|
|
167
|
+
id, label, version, abis: parts.map(({ arch }) => arch), icon: icons ? '@mipmap/ic_launcher' : '@android:drawable/sym_def_app_icon',
|
|
168
|
+
launchMode: urlSchemes.length > 0 ? 'singleTask' : 'standard', theme: background ? '@style/CrossbindBackground' : DAY_NIGHT,
|
|
108
169
|
}));
|
|
109
170
|
return to;
|
|
110
171
|
}
|
package/src/actions/licenses.js
CHANGED
|
@@ -214,6 +214,7 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
214
214
|
const WEBVIEW = { version: 'cbbdee44afff22867de9fd88a9fc8350d9bdd399', sha256: '10e972a2327b5681474f4aa4499e505eaa4ff659aa995380386f08fd6fc1b763' };
|
|
215
215
|
const WEBVIEW2_SDK = { version: '1.0.4258.31', sha256: '56f7f4b8bf9aee4b8efefbbdd4f67d5f74ebd1b100ed0806da71bf76af481aa9' };
|
|
216
216
|
const JSI_SHA256 = 'cb6274a22abc93ecc970066539815e716d3e47716e8f93a9971cf4eb5918ffc1';
|
|
217
|
+
const MONOCYPHER = { version: '4.0.3', sha256: '8cc9bc341a66249016db9bd70e9142d8d0aef9945973744b1ac05dbc55d8ee66' };
|
|
217
218
|
// The fbjni the Hermes release for Android depends on.
|
|
218
219
|
const FBJNI_VERSION = '0.7.0';
|
|
219
220
|
|
|
@@ -272,8 +273,8 @@ function androidAppRows() {
|
|
|
272
273
|
];
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
// The WebView2 SDK headers a Windows host compiles against
|
|
276
|
-
// the SDK's loader.
|
|
276
|
+
// The WebView2 SDK headers a Windows host compiles against, and the NSIS its setup program carries. webview/webview
|
|
277
|
+
// loads the WebView2 Runtime itself, without the SDK's loader.
|
|
277
278
|
function windowsAppRows(hostRoot) {
|
|
278
279
|
const license = 'BSD-3-Clause';
|
|
279
280
|
return [{
|
|
@@ -289,6 +290,19 @@ function windowsAppRows(hostRoot) {
|
|
|
289
290
|
sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
|
|
290
291
|
licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
|
|
291
292
|
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')),
|
|
292
306
|
}];
|
|
293
307
|
}
|
|
294
308
|
|
|
@@ -336,6 +350,21 @@ function appRuntimeRows(platform) {
|
|
|
336
350
|
licenseNotes: 'webview/webview, with the local patch @crossbind/core-app-host lists',
|
|
337
351
|
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
|
|
338
352
|
}]),
|
|
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
|
+
}]),
|
|
339
368
|
];
|
|
340
369
|
}
|
|
341
370
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import state from '../state/index.js';
|
|
5
|
+
import getOsUserAndGroupId from '../utils/getOsUserAndGroupId.js';
|
|
6
|
+
import pullDockerImage, { getDockerImage } from '../utils/pullDockerImage.js';
|
|
7
|
+
import replaceBasePathForDocker, { DOCKER_BASE } from '../utils/replaceBasePathForDocker.js';
|
|
8
|
+
import { DOCKER_RUN_SECURITY_ARGS } from '../utils/dockerSecurity.js';
|
|
9
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
10
|
+
|
|
11
|
+
// The key that signs a Windows app and its setup program, named by the environment alone so that no secret reaches a
|
|
12
|
+
// file or a command line: jsign's own options, which the windows image's jsign takes through the host's jsign.sh.
|
|
13
|
+
export const WINDOWS_SIGNING = {
|
|
14
|
+
storetype: 'CROSSBIND_WINDOWS_SIGNING_STORETYPE',
|
|
15
|
+
keystore: 'CROSSBIND_WINDOWS_SIGNING_KEYSTORE',
|
|
16
|
+
alias: 'CROSSBIND_WINDOWS_SIGNING_ALIAS',
|
|
17
|
+
storepass: 'CROSSBIND_WINDOWS_SIGNING_STOREPASS',
|
|
18
|
+
keypass: 'CROSSBIND_WINDOWS_SIGNING_KEYPASS',
|
|
19
|
+
certfile: 'CROSSBIND_WINDOWS_SIGNING_CERTFILE',
|
|
20
|
+
tsaurl: 'CROSSBIND_WINDOWS_SIGNING_TSAURL',
|
|
21
|
+
};
|
|
22
|
+
// The store types of the image's jsign 7.6: files, which the container reads mounted, cloud key services, and hardware
|
|
23
|
+
// tokens, which it does not reach.
|
|
24
|
+
const FILE_STORES = ['PKCS12', 'JKS', 'JCEKS'];
|
|
25
|
+
const CLOUDS = ['AWS', 'AZUREKEYVAULT', 'DIGICERTONE', 'ESIGNER', 'GARASIGN', 'GOOGLECLOUD', 'HASHICORPVAULT', 'ORACLECLOUD', 'SIGNPATH', 'SIGNSERVER', 'TRUSTEDSIGNING'];
|
|
26
|
+
const TOKENS = ['PKCS11', 'CRYPTOCERTUM', 'ETOKEN', 'NITROKEY', 'OPENPGP', 'OPENSC', 'PIV', 'YUBIKEY'];
|
|
27
|
+
// Where the container reads the files the variables name.
|
|
28
|
+
const FILES = '/run/crossbind/signing';
|
|
29
|
+
const OUTPUT_LIMIT = 64 * 1024 * 1024;
|
|
30
|
+
const FAILURE_LINES = 40;
|
|
31
|
+
|
|
32
|
+
const listed = (names) => `${names.join(' and ')} ${names.length > 1 ? 'are' : 'is'}`;
|
|
33
|
+
|
|
34
|
+
function existingFile(name, env) {
|
|
35
|
+
const file = path.resolve(env[name] ?? '');
|
|
36
|
+
if (!env[name] || !fs.statSync(file, { throwIfNoEntry: false })?.isFile()) {
|
|
37
|
+
throw new Error(`crossbind: ${name} names ${env[name] ? file : 'nothing'}, which is no file.`);
|
|
38
|
+
}
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The key a Windows app is signed with, or null without CROSSBIND_WINDOWS_SIGNING_STORETYPE: the app and its setup
|
|
43
|
+
// program are then not signed.
|
|
44
|
+
export function windowsSigning(env = process.env) {
|
|
45
|
+
const set = Object.values(WINDOWS_SIGNING).filter((name) => env[name]);
|
|
46
|
+
const storetype = env[WINDOWS_SIGNING.storetype]?.toUpperCase();
|
|
47
|
+
if (!storetype) {
|
|
48
|
+
if (set.length) throw new Error(`crossbind: ${listed(set)} set without ${WINDOWS_SIGNING.storetype}, the kind of key jsign signs a Windows app with.`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
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 Trusted Signing (TRUSTEDSIGNING) or Azure Key Vault (AZUREKEYVAULT), or with a PKCS#12 file.`);
|
|
53
|
+
}
|
|
54
|
+
if (![...FILE_STORES, ...CLOUDS].includes(storetype)) {
|
|
55
|
+
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is no store type jsign takes: ${[...FILE_STORES, ...CLOUDS].join(', ')}.`);
|
|
56
|
+
}
|
|
57
|
+
const files = {};
|
|
58
|
+
if (FILE_STORES.includes(storetype)) files.keystore = existingFile(WINDOWS_SIGNING.keystore, env);
|
|
59
|
+
if (env[WINDOWS_SIGNING.certfile]) files.certfile = existingFile(WINDOWS_SIGNING.certfile, env);
|
|
60
|
+
return { names: set, files };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A command in crossbind's windows image, which sees the project's base, with the signing key's files mounted
|
|
64
|
+
// read-only under their own names and its other variables passed by name, so that their values stay off the command
|
|
65
|
+
// line. A failure names the tool it was, as the label.
|
|
66
|
+
export function inWindowsImage(command, workdir, signing = null, label = command[0]) {
|
|
67
|
+
const { base } = state.config.paths;
|
|
68
|
+
pullDockerImage('windows');
|
|
69
|
+
const files = Object.entries(signing?.files ?? {});
|
|
70
|
+
const mounted = files.map(([key]) => WINDOWS_SIGNING[key]);
|
|
71
|
+
const args = [
|
|
72
|
+
'run', '--rm', ...DOCKER_RUN_SECURITY_ARGS, '--user', getOsUserAndGroupId(), '-v', `${base}:${DOCKER_BASE}`,
|
|
73
|
+
...files.flatMap(([key, file]) => ['-v', `${file}:${FILES}/${key}:ro`, '-e', `${WINDOWS_SIGNING[key]}=${FILES}/${key}`]),
|
|
74
|
+
...(signing?.names ?? []).filter((name) => !mounted.includes(name)).flatMap((name) => ['-e', name]),
|
|
75
|
+
// makensis reads its arguments in the locale's encoding, which the image leaves unset.
|
|
76
|
+
'-e', 'LC_ALL=C.UTF-8',
|
|
77
|
+
'-w', replaceBasePathForDocker(workdir, base),
|
|
78
|
+
getDockerImage('windows'),
|
|
79
|
+
...replaceBasePathForDocker(command, base),
|
|
80
|
+
];
|
|
81
|
+
const result = spawnSync('docker', args, { encoding: 'utf8', maxBuffer: OUTPUT_LIMIT });
|
|
82
|
+
if (result.error) throw new Error(`crossbind: cannot run docker for ${label}: ${result.error.message}`, { cause: result.error });
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().split('\n').slice(-FAILURE_LINES).join('\n');
|
|
85
|
+
const end = result.signal ? `on ${result.signal}` : `with exit code ${result.status}`;
|
|
86
|
+
throw new Error(`crossbind: ${label} failed in the windows image ${end}:\n${output}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The script the windows image signs a file with, which a setup program's build runs too.
|
|
91
|
+
export const jsignScript = () => `${resolveAppHostRoot()}/windows/jsign.sh`;
|
|
92
|
+
|
|
93
|
+
// Signs Windows executables in place with the key the environment names.
|
|
94
|
+
export function signWindows(files, signing) {
|
|
95
|
+
inWindowsImage(['sh', jsignScript(), ...files], path.dirname(files[0]), signing, 'jsign');
|
|
96
|
+
}
|
|
@@ -18,7 +18,7 @@ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
|
|
|
18
18
|
|
|
19
19
|
const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
|
|
20
20
|
'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
|
|
21
|
-
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs'];
|
|
21
|
+
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild'];
|
|
22
22
|
// Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
|
|
23
23
|
const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
|
|
24
24
|
|
|
@@ -36,7 +36,10 @@ const PROJECT_SETTINGS = {
|
|
|
36
36
|
|
|
37
37
|
const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// The color set of app.window.backgroundColor, which the launch screen shows.
|
|
40
|
+
const LAUNCH_COLOR = 'LaunchBackground';
|
|
41
|
+
|
|
42
|
+
const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
40
43
|
CFBundleDevelopmentRegion: 'en',
|
|
41
44
|
CFBundleDisplayName: name,
|
|
42
45
|
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
@@ -45,22 +48,31 @@ const infoPlist = (name) => xmlPlist({
|
|
|
45
48
|
CFBundleName: '$(PRODUCT_NAME)',
|
|
46
49
|
CFBundlePackageType: 'APPL',
|
|
47
50
|
CFBundleShortVersionString: '$(MARKETING_VERSION)',
|
|
51
|
+
...(urlSchemes.length > 0 ? { CFBundleURLTypes: [{ CFBundleURLName: '$(PRODUCT_BUNDLE_IDENTIFIER)', CFBundleURLSchemes: urlSchemes }] } : {}),
|
|
48
52
|
CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
49
53
|
LSRequiresIPhoneOS: true,
|
|
50
54
|
UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
|
|
51
|
-
UILaunchScreen: {},
|
|
55
|
+
UILaunchScreen: hasBackground ? { UIColorName: LAUNCH_COLOR } : {},
|
|
52
56
|
UISupportedInterfaceOrientations: ORIENTATIONS,
|
|
53
57
|
'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
|
|
58
|
+
// The reasons of app.permissions, without which iOS lets the app use none of those devices.
|
|
59
|
+
...usage,
|
|
54
60
|
});
|
|
55
61
|
|
|
56
|
-
function objects(name, targetSettings) {
|
|
62
|
+
function objects(name, targetSettings, hasAssets) {
|
|
57
63
|
const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
|
|
58
64
|
const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
|
|
59
65
|
const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
|
|
60
66
|
const configurations = (debug, release) => ({
|
|
61
67
|
isa: 'XCConfigurationList', buildConfigurations: [debug, release], defaultConfigurationIsVisible: 0, defaultConfigurationName: 'Release',
|
|
62
68
|
});
|
|
69
|
+
// The app's icon and launch screen color, in an asset catalog.
|
|
70
|
+
const assets = hasAssets ? {
|
|
71
|
+
[ID.assets]: file('Assets.xcassets', 'folder.assetcatalog'),
|
|
72
|
+
[ID.assetsBuild]: { isa: 'PBXBuildFile', fileRef: ID.assets },
|
|
73
|
+
} : {};
|
|
63
74
|
return {
|
|
75
|
+
...assets,
|
|
64
76
|
[ID.main]: file('main.m', 'sourcecode.c.objc'),
|
|
65
77
|
[ID.info]: file('Info.plist', 'text.plist.xml'),
|
|
66
78
|
[ID.app]: file('app', 'folder'),
|
|
@@ -74,9 +86,11 @@ function objects(name, targetSettings) {
|
|
|
74
86
|
[ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
|
|
75
87
|
[ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
|
|
76
88
|
[ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
|
|
77
|
-
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild]),
|
|
89
|
+
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : [])]),
|
|
78
90
|
[ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
|
|
79
|
-
[ID.mainGroup]: {
|
|
91
|
+
[ID.mainGroup]: {
|
|
92
|
+
isa: 'PBXGroup', children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ID.hermes, ID.products], sourceTree: '<group>',
|
|
93
|
+
},
|
|
80
94
|
[ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
|
|
81
95
|
[ID.target]: {
|
|
82
96
|
isa: 'PBXNativeTarget',
|
|
@@ -111,10 +125,28 @@ function objects(name, targetSettings) {
|
|
|
111
125
|
};
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
// The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color.
|
|
129
|
+
function writeAssets(catalog, icon, background) {
|
|
130
|
+
const info = { author: 'xcode', version: 1 };
|
|
131
|
+
const assetSet = (set, contents) => {
|
|
132
|
+
fs.mkdirSync(`${catalog}/${set}`, { recursive: true });
|
|
133
|
+
fs.writeFileSync(`${catalog}/Contents.json`, JSON.stringify({ info }, null, 2));
|
|
134
|
+
fs.writeFileSync(`${catalog}/${set}/Contents.json`, JSON.stringify({ ...contents, info }, null, 2));
|
|
135
|
+
};
|
|
136
|
+
if (icon) {
|
|
137
|
+
assetSet('AppIcon.appiconset', { images: [{ filename: 'icon.png', idiom: 'universal', platform: 'ios', size: '1024x1024' }] });
|
|
138
|
+
fs.writeFileSync(`${catalog}/AppIcon.appiconset/icon.png`, icon);
|
|
139
|
+
}
|
|
140
|
+
if (background) {
|
|
141
|
+
const [red, green, blue] = [1, 3, 5].map((at) => `0x${background.slice(at, at + 2).toUpperCase()}`);
|
|
142
|
+
assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } }, idiom: 'universal' }] });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
// An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
|
|
115
147
|
// their link line, and its slice of Hermes; the first also the app's files. Xcode signs the app and runs it, in the
|
|
116
|
-
// Simulator or on a device.
|
|
117
|
-
export default function writeXcodeProject(to, { parts, identity, version }) {
|
|
148
|
+
// Simulator or on a device, and makes every size of its icon from the one PNG given.
|
|
149
|
+
export default function writeXcodeProject(to, { parts, identity, label, version, icon = null, urlSchemes = [], background = null, usage = {} }) {
|
|
118
150
|
const { name } = state.config.general;
|
|
119
151
|
if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
|
|
120
152
|
fs.rmSync(to, { recursive: true, force: true });
|
|
@@ -133,8 +165,10 @@ export default function writeXcodeProject(to, { parts, identity, version }) {
|
|
|
133
165
|
if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
|
|
134
166
|
|
|
135
167
|
fs.writeFileSync(`${to}/main.m`, MAIN);
|
|
136
|
-
fs.writeFileSync(`${to}/Info.plist`, infoPlist(
|
|
168
|
+
fs.writeFileSync(`${to}/Info.plist`, infoPlist(label, urlSchemes, Boolean(background), usage));
|
|
169
|
+
writeAssets(`${to}/Assets.xcassets`, icon, background);
|
|
137
170
|
const targetSettings = {
|
|
171
|
+
...(icon ? { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon' } : {}),
|
|
138
172
|
PRODUCT_NAME: name,
|
|
139
173
|
PRODUCT_BUNDLE_IDENTIFIER: identity.id,
|
|
140
174
|
DEVELOPMENT_TEAM: identity.team,
|
|
@@ -146,7 +180,7 @@ export default function writeXcodeProject(to, { parts, identity, version }) {
|
|
|
146
180
|
...linkFlags,
|
|
147
181
|
};
|
|
148
182
|
fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
|
|
149
|
-
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings), rootObject: ID.project,
|
|
183
|
+
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background)), rootObject: ID.project,
|
|
150
184
|
}));
|
|
151
185
|
return to;
|
|
152
186
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// What an app's host does for its page (target.runtimeEnv: 'app'); in any other build every call rejects.
|
|
2
|
+
|
|
3
|
+
/** A kind of file a dialog offers: its name, and the extensions, with or without the dot, that make it; "*" is any file. */
|
|
4
|
+
export interface FileFilter {
|
|
5
|
+
name: string;
|
|
6
|
+
extensions: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OpenOptions {
|
|
10
|
+
title?: string;
|
|
11
|
+
/** The folder the dialog shows first. */
|
|
12
|
+
defaultPath?: string;
|
|
13
|
+
filters?: FileFilter[];
|
|
14
|
+
/** Folders instead of files. */
|
|
15
|
+
directory?: boolean;
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SaveOptions {
|
|
20
|
+
title?: string;
|
|
21
|
+
/** The folder the dialog shows first. */
|
|
22
|
+
defaultPath?: string;
|
|
23
|
+
defaultName?: string;
|
|
24
|
+
filters?: FileFilter[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MessageOptions {
|
|
28
|
+
message: string;
|
|
29
|
+
/** Smaller text under the message. */
|
|
30
|
+
detail?: string;
|
|
31
|
+
/** info by default. Windows shows its icon for each, and macOS a caution sign for error; Linux, iOS and Android show none. */
|
|
32
|
+
kind?: 'info' | 'warning' | 'error';
|
|
33
|
+
/** One to three labels, the first the default; without them OK. */
|
|
34
|
+
buttons?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The app's own folders, made when first asked for: what it keeps, what it can lose, and its logs. */
|
|
38
|
+
export interface AppPaths {
|
|
39
|
+
data: string;
|
|
40
|
+
cache: string;
|
|
41
|
+
logs: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare const dialog: {
|
|
45
|
+
/** The paths of the files or folders the user picked, or null when they cancelled. */
|
|
46
|
+
open(options?: OpenOptions): Promise<string[] | null>;
|
|
47
|
+
/** The path the user chose to save to, or null when they cancelled. */
|
|
48
|
+
save(options?: SaveOptions): Promise<string | null>;
|
|
49
|
+
/** The index of the button the user clicked: Return gives the first, and Escape or closing the dialog the last. */
|
|
50
|
+
message(options: MessageOptions): Promise<number>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export declare const shell: {
|
|
54
|
+
/** An http, https or mailto address in the default browser or mail app. */
|
|
55
|
+
openExternal(url: string | URL): Promise<void>;
|
|
56
|
+
/** A file in the app the system opens its kind with, where app.openPath in crossbind.config.js lets the page: nowhere without it. */
|
|
57
|
+
openPath(path: string): Promise<void>;
|
|
58
|
+
/** A file, selected, in the system's file manager: one in the app's folders, or one a dialog picked during this run. */
|
|
59
|
+
revealInFolder(path: string): Promise<void>;
|
|
60
|
+
/** A file or folder moved to the system's trash, where its user can take it back: one in the app's folders, not one of them itself, or one a dialog picked during this run. Where the system has no trash, nothing moves and it rejects. */
|
|
61
|
+
trash(path: string): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export declare const app: {
|
|
65
|
+
paths(): Promise<AppPaths>;
|
|
66
|
+
/** Ends the app as closing its window does. */
|
|
67
|
+
quit(): Promise<void>;
|
|
68
|
+
/** Ends the app as quit() does, then opens it again, with no link. */
|
|
69
|
+
relaunch(): Promise<void>;
|
|
70
|
+
/** The links of app.urlSchemes in crossbind.config.js that opened the app and have not reached the page yet, the one it was launched with among them. Once the page has asked, each later link comes as the event open-url while it listens, so listening first, then asking, misses none. */
|
|
71
|
+
launchUrls(): Promise<string[]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** A key with CmdOrCtrl, Ctrl or Alt and maybe Shift, joined by +, as CmdOrCtrl+Shift+O is, or F1–F12, alone or with any of them. The key is A–Z, 0–9, F1–F12, Enter, Escape, Tab, Space, Backspace, Delete, Up, Down, Left, Right, Home, End, PageUp, PageDown, Plus, Minus, Comma or Period. */
|
|
75
|
+
export type Accelerator = string;
|
|
76
|
+
|
|
77
|
+
/** An item of a menu: one the page handles, by its id, a submenu, a separator, or a standard one by its role. */
|
|
78
|
+
export type MenuItem =
|
|
79
|
+
| { label: string; id?: string; accelerator?: Accelerator; enabled?: boolean; checked?: boolean }
|
|
80
|
+
| { label: string; items: MenuItem[] }
|
|
81
|
+
| { type: 'separator' }
|
|
82
|
+
| { role: 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'selectAll'; label?: string; accelerator?: never }
|
|
83
|
+
| { role: 'minimize' | 'close' | 'quit'; label?: string; accelerator?: Accelerator }
|
|
84
|
+
| { role: 'editMenu' | 'windowMenu'; label?: string };
|
|
85
|
+
|
|
86
|
+
/** A menu of the bar, by its items, or a standard one by its role. */
|
|
87
|
+
export type Menu = { label: string; items: MenuItem[] } | { role: 'editMenu' | 'windowMenu'; label?: string };
|
|
88
|
+
|
|
89
|
+
/** What the event menu carries: the id of the item clicked, and for one with checked whether it now is. */
|
|
90
|
+
export interface MenuClick {
|
|
91
|
+
id: string;
|
|
92
|
+
checked?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export declare const menu: {
|
|
96
|
+
/** Sets the app's menus: on macOS after its app menu, which the app keeps, and on Windows and Linux in a bar of its window. A click of an item with an id is an event, menu, with a MenuClick. */
|
|
97
|
+
set(menus: Menu[]): Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** An item of the tray's menu: one the page handles, by its id, a submenu or a separator. */
|
|
101
|
+
export type TrayItem =
|
|
102
|
+
| { label: string; id?: string; enabled?: boolean; checked?: boolean }
|
|
103
|
+
| { label: string; items: TrayItem[] }
|
|
104
|
+
| { type: 'separator' };
|
|
105
|
+
|
|
106
|
+
export interface TrayOptions {
|
|
107
|
+
/** What the icon shows when the pointer rests on it. */
|
|
108
|
+
tooltip?: string;
|
|
109
|
+
/** The menu a click of the icon opens: a click on macOS, a right click on Windows, and on Linux the one its desktop takes. */
|
|
110
|
+
menu?: TrayItem[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export declare const tray: {
|
|
114
|
+
/** The app's icon in the macOS menu bar, the Windows notification area or a Linux desktop's status tray. A click of an item with an id is the event menu, with a MenuClick, and a click of the icon the event tray where it opens no menu. Set again, it changes the icon that shows. */
|
|
115
|
+
set(options?: TrayOptions): Promise<void>;
|
|
116
|
+
/** Takes the icon away. */
|
|
117
|
+
remove(): Promise<void>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export interface NotificationOptions {
|
|
121
|
+
title: string;
|
|
122
|
+
body?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export declare const notification: {
|
|
126
|
+
/** A notification of the system's; a click on it is the event notification. macOS asks its user first. */
|
|
127
|
+
show(options: NotificationOptions): Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** What the event second-instance carries: a later launch's arguments, the program's own left out, and the folder it was launched in. */
|
|
131
|
+
export interface SecondInstance {
|
|
132
|
+
args: string[];
|
|
133
|
+
cwd: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What the event open-url carries: a link of one of app.urlSchemes that the system opened the app with. */
|
|
137
|
+
export interface OpenUrl {
|
|
138
|
+
url: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export declare const events: {
|
|
142
|
+
/** Calls listener with each click of an item with an id in the menus menu.set sets or in the tray's menu, until the function it returns is called. */
|
|
143
|
+
on(name: 'menu', listener: (click: MenuClick) => void): () => void;
|
|
144
|
+
/** Calls listener with each link of app.urlSchemes the system opens the app with once the page has called app.launchUrls(), until the function it returns is called. */
|
|
145
|
+
on(name: 'open-url', listener: (open: OpenUrl) => void): () => void;
|
|
146
|
+
/** Calls listener with each later launch of a desktop app that app.singleInstance keeps to one instance, which brings its window to the front as far as the system lets it, until the function it returns is called. */
|
|
147
|
+
on(name: 'second-instance', listener: (launch: SecondInstance) => void): () => void;
|
|
148
|
+
/** Calls listener as the system of a desktop app goes to sleep, which it may do before the page has run, until the function it returns is called. */
|
|
149
|
+
on(name: 'suspend', listener: () => void): () => void;
|
|
150
|
+
/** Calls listener as the system of a desktop app wakes from sleep, until the function it returns is called. */
|
|
151
|
+
on(name: 'resume', listener: () => void): () => void;
|
|
152
|
+
/** Calls listener with each left or right click of the tray's icon that opens no menu, until the function it returns is called. */
|
|
153
|
+
on(name: 'tray', listener: () => void): () => void;
|
|
154
|
+
/** Calls listener with each click on a notification the app's notification.show showed while it runs, until the function it returns is called. */
|
|
155
|
+
on(name: 'notification', listener: () => void): () => void;
|
|
156
|
+
/** Calls listener with the value of each event of this name the app's native code emits from now on, until the function it returns is called. */
|
|
157
|
+
on(name: string, listener: (value: unknown) => void): () => void;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** A newer version of the app, which app.updater.url in crossbind.config.js offers. */
|
|
161
|
+
export interface Update {
|
|
162
|
+
version: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One check or install runs at a time, and another rejects meanwhile. */
|
|
166
|
+
export declare const updater: {
|
|
167
|
+
/** The version app.updater.url offers when it is newer than the app's, or null. */
|
|
168
|
+
check(): Promise<Update | null>;
|
|
169
|
+
/** Downloads that version, checks that the key of app.updater.publicKey signed it and installs it in place of the app, which then quits and opens the new version. It rejects when that fails. */
|
|
170
|
+
install(): Promise<void>;
|
|
171
|
+
};
|