crossbind 2.0.0-beta.66 → 2.0.0-beta.68
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 +32 -23
- package/package.json +2 -2
- package/src/actions/androidGradle.js +44 -9
- package/src/actions/appInstaller.js +8 -5
- package/src/actions/appSetup.js +116 -0
- package/src/actions/appStoreConnect.js +188 -0
- package/src/actions/appleSigning.js +32 -14
- package/src/actions/buildApp.js +83 -16
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildExternal.js +8 -1
- package/src/actions/createLib.js +5 -3
- package/src/actions/getCmakeParameters.js +2 -3
- package/src/actions/googlePlay.js +213 -0
- package/src/actions/googleSignIn.js +78 -0
- package/src/actions/gradleProject.js +3 -0
- package/src/actions/iosDevice.js +200 -0
- package/src/actions/playSetup.js +19 -0
- package/src/actions/publishAndroid.js +102 -0
- package/src/actions/publishIos.js +146 -0
- package/src/actions/uploadKey.js +134 -0
- package/src/actions/xcodeProject.js +30 -9
- package/src/app/index.d.ts +43 -1
- package/src/app/index.js +64 -3
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +81 -20
- package/src/index.js +3 -0
- package/src/program.js +3 -1
- package/src/utils/appApiHeader.js +4 -0
- package/src/utils/appOptions.js +77 -0
- package/src/utils/ask.js +38 -0
- package/src/utils/configWriter.js +64 -0
- package/src/utils/embindArgumentGuards.js +8 -5
- package/src/utils/embindCallArguments.js +6 -6
- package/src/utils/hermes.js +24 -0
- package/src/utils/keychain.js +54 -0
- package/src/utils/openUrl.js +11 -0
- package/src/utils/overrideDependency.js +3 -0
- package/src/utils/password.js +34 -0
- package/src/utils/recipePatches.js +61 -0
- package/src/utils/requiredReasonApis.js +77 -0
- package/src/utils/secretFile.js +23 -0
- package/src/utils/sourceFingerprint.js +8 -1
- package/src/utils/systemConfig.js +6 -0
- package/src/utils/systemKeys.js +16 -0
|
@@ -3,6 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import { IOS_DEPLOYMENT_TARGET } from '../utils/iosToolchain.js';
|
|
5
5
|
import { textPlist, xmlPlist } from '../utils/plist.js';
|
|
6
|
+
import { PRIVACY_CATEGORIES } from '../utils/appOptions.js';
|
|
6
7
|
|
|
7
8
|
// The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
|
|
8
9
|
export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
|
|
@@ -18,7 +19,7 @@ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
|
|
|
18
19
|
|
|
19
20
|
const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
|
|
20
21
|
'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
|
|
21
|
-
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild'];
|
|
22
|
+
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild', 'privacy', 'privacyBuild'];
|
|
22
23
|
// Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
|
|
23
24
|
const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
|
|
24
25
|
|
|
@@ -39,7 +40,7 @@ const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationL
|
|
|
39
40
|
// The color set of app.window.backgroundColor, which the launch screen shows.
|
|
40
41
|
const LAUNCH_COLOR = 'LaunchBackground';
|
|
41
42
|
|
|
42
|
-
const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
43
|
+
const infoPlist = (name, urlSchemes, hasBackground, usage, encryption) => xmlPlist({
|
|
43
44
|
CFBundleDevelopmentRegion: 'en',
|
|
44
45
|
CFBundleDisplayName: name,
|
|
45
46
|
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
@@ -50,6 +51,7 @@ const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
|
50
51
|
CFBundleShortVersionString: '$(MARKETING_VERSION)',
|
|
51
52
|
...(urlSchemes.length > 0 ? { CFBundleURLTypes: [{ CFBundleURLName: '$(PRODUCT_BUNDLE_IDENTIFIER)', CFBundleURLSchemes: urlSchemes }] } : {}),
|
|
52
53
|
CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
54
|
+
...(encryption === null ? {} : { ITSAppUsesNonExemptEncryption: encryption }),
|
|
53
55
|
LSRequiresIPhoneOS: true,
|
|
54
56
|
UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
|
|
55
57
|
UILaunchScreen: hasBackground ? { UIColorName: LAUNCH_COLOR } : {},
|
|
@@ -59,7 +61,7 @@ const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
|
59
61
|
...usage,
|
|
60
62
|
});
|
|
61
63
|
|
|
62
|
-
function objects(name, targetSettings, hasAssets) {
|
|
64
|
+
function objects(name, targetSettings, hasAssets, hasPrivacy) {
|
|
63
65
|
const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
|
|
64
66
|
const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
|
|
65
67
|
const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
|
|
@@ -71,8 +73,13 @@ function objects(name, targetSettings, hasAssets) {
|
|
|
71
73
|
[ID.assets]: file('Assets.xcassets', 'folder.assetcatalog'),
|
|
72
74
|
[ID.assetsBuild]: { isa: 'PBXBuildFile', fileRef: ID.assets },
|
|
73
75
|
} : {};
|
|
76
|
+
const privacy = hasPrivacy ? {
|
|
77
|
+
[ID.privacy]: file('PrivacyInfo.xcprivacy', 'text.xml'),
|
|
78
|
+
[ID.privacyBuild]: { isa: 'PBXBuildFile', fileRef: ID.privacy },
|
|
79
|
+
} : {};
|
|
74
80
|
return {
|
|
75
81
|
...assets,
|
|
82
|
+
...privacy,
|
|
76
83
|
[ID.main]: file('main.m', 'sourcecode.c.objc'),
|
|
77
84
|
[ID.info]: file('Info.plist', 'text.plist.xml'),
|
|
78
85
|
[ID.app]: file('app', 'folder'),
|
|
@@ -86,10 +93,12 @@ function objects(name, targetSettings, hasAssets) {
|
|
|
86
93
|
[ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
|
|
87
94
|
[ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
|
|
88
95
|
[ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
|
|
89
|
-
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : [])]),
|
|
96
|
+
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : []), ...(hasPrivacy ? [ID.privacyBuild] : [])]),
|
|
90
97
|
[ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
|
|
91
98
|
[ID.mainGroup]: {
|
|
92
|
-
isa: 'PBXGroup',
|
|
99
|
+
isa: 'PBXGroup',
|
|
100
|
+
children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ...(hasPrivacy ? [ID.privacy] : []), ID.hermes, ID.products],
|
|
101
|
+
sourceTree: '<group>',
|
|
93
102
|
},
|
|
94
103
|
[ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
|
|
95
104
|
[ID.target]: {
|
|
@@ -143,10 +152,20 @@ function writeAssets(catalog, icon, background) {
|
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
// The privacy manifest of app.privacy, without which App Store Connect refuses an app that calls required reason API.
|
|
156
|
+
const privacyManifest = (privacy) => xmlPlist({
|
|
157
|
+
NSPrivacyAccessedAPITypes: Object.entries(privacy).map(([name, reasons]) => ({
|
|
158
|
+
NSPrivacyAccessedAPIType: PRIVACY_CATEGORIES[name].type,
|
|
159
|
+
NSPrivacyAccessedAPITypeReasons: reasons,
|
|
160
|
+
})),
|
|
161
|
+
});
|
|
162
|
+
|
|
146
163
|
// An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
|
|
147
164
|
// their link line, and its slice of Hermes; the first also the app's files. Xcode signs the app and runs it, in the
|
|
148
165
|
// Simulator or on a device, and makes every size of its icon from the one PNG given.
|
|
149
|
-
export default function writeXcodeProject(to, {
|
|
166
|
+
export default function writeXcodeProject(to, {
|
|
167
|
+
parts, identity, label, version, build = version, icon = null, urlSchemes = [], background = null, usage = {}, encryption = null, privacy = {},
|
|
168
|
+
}) {
|
|
150
169
|
const { name } = state.config.general;
|
|
151
170
|
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.`);
|
|
152
171
|
fs.rmSync(to, { recursive: true, force: true });
|
|
@@ -165,8 +184,10 @@ export default function writeXcodeProject(to, { parts, identity, label, version,
|
|
|
165
184
|
if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
|
|
166
185
|
|
|
167
186
|
fs.writeFileSync(`${to}/main.m`, MAIN);
|
|
168
|
-
fs.writeFileSync(`${to}/Info.plist`, infoPlist(label, urlSchemes, Boolean(background), usage));
|
|
187
|
+
fs.writeFileSync(`${to}/Info.plist`, infoPlist(label, urlSchemes, Boolean(background), usage, encryption));
|
|
169
188
|
writeAssets(`${to}/Assets.xcassets`, icon, background);
|
|
189
|
+
const hasPrivacy = Object.keys(privacy).length > 0;
|
|
190
|
+
if (hasPrivacy) fs.writeFileSync(`${to}/PrivacyInfo.xcprivacy`, privacyManifest(privacy));
|
|
170
191
|
const targetSettings = {
|
|
171
192
|
...(icon ? { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon' } : {}),
|
|
172
193
|
PRODUCT_NAME: name,
|
|
@@ -175,12 +196,12 @@ export default function writeXcodeProject(to, { parts, identity, label, version,
|
|
|
175
196
|
CODE_SIGN_STYLE: 'Automatic',
|
|
176
197
|
INFOPLIST_FILE: 'Info.plist',
|
|
177
198
|
MARKETING_VERSION: version,
|
|
178
|
-
CURRENT_PROJECT_VERSION:
|
|
199
|
+
CURRENT_PROJECT_VERSION: build,
|
|
179
200
|
LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
|
|
180
201
|
...linkFlags,
|
|
181
202
|
};
|
|
182
203
|
fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
|
|
183
|
-
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background)), rootObject: ID.project,
|
|
204
|
+
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background), hasPrivacy), rootObject: ID.project,
|
|
184
205
|
}));
|
|
185
206
|
return to;
|
|
186
207
|
}
|
package/src/app/index.d.ts
CHANGED
|
@@ -57,12 +57,16 @@ export declare const shell: {
|
|
|
57
57
|
openPath(path: string): Promise<void>;
|
|
58
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
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>;
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
export declare const app: {
|
|
63
65
|
paths(): Promise<AppPaths>;
|
|
64
66
|
/** Ends the app as closing its window does. */
|
|
65
67
|
quit(): Promise<void>;
|
|
68
|
+
/** Ends the app as quit() does, then opens it again, with no link. */
|
|
69
|
+
relaunch(): Promise<void>;
|
|
66
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. */
|
|
67
71
|
launchUrls(): Promise<string[]>;
|
|
68
72
|
};
|
|
@@ -93,6 +97,36 @@ export declare const menu: {
|
|
|
93
97
|
set(menus: Menu[]): Promise<void>;
|
|
94
98
|
};
|
|
95
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
|
+
|
|
96
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. */
|
|
97
131
|
export interface SecondInstance {
|
|
98
132
|
args: string[];
|
|
@@ -105,12 +139,20 @@ export interface OpenUrl {
|
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
export declare const events: {
|
|
108
|
-
/** Calls listener with each click of an item with an id in the menus menu.set sets, until the function it returns is called. */
|
|
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. */
|
|
109
143
|
on(name: 'menu', listener: (click: MenuClick) => void): () => void;
|
|
110
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. */
|
|
111
145
|
on(name: 'open-url', listener: (open: OpenUrl) => void): () => void;
|
|
112
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. */
|
|
113
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;
|
|
114
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. */
|
|
115
157
|
on(name: string, listener: (value: unknown) => void): () => void;
|
|
116
158
|
};
|
package/src/app/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// What an app's host does for its page: the dialogs that pick files or show a message, opening an address or a file
|
|
2
|
-
// outside the app, the app's folders, quitting, its menus,
|
|
3
|
-
// updates. Each call goes to the host of an app (target.runtimeEnv: 'app'); in any
|
|
4
|
-
// throws. The app's native code calls the same through <crossbind/app.h>, but for
|
|
2
|
+
// outside the app, the app's folders, quitting, its menus, its tray icon and notifications, the events of the app's
|
|
3
|
+
// native code and a desktop app's updates. Each call goes to the host of an app (target.runtimeEnv: 'app'); in any
|
|
4
|
+
// other build it rejects, or events.on throws. The app's native code calls the same through <crossbind/app.h>, but for
|
|
5
|
+
// the menus, the tray, notifications and events.on.
|
|
5
6
|
|
|
6
7
|
const OPEN = { title: 'string', defaultPath: 'string', filters: 'filters', directory: 'boolean', multiple: 'boolean' };
|
|
7
8
|
const SAVE = { title: 'string', defaultPath: 'string', defaultName: 'string', filters: 'filters' };
|
|
@@ -32,6 +33,7 @@ const isButtons = (value) => Array.isArray(value) && value.length >= 1 && value.
|
|
|
32
33
|
const LISTS = {
|
|
33
34
|
filters: { isFitting: isFilters, name: 'list of { name, extensions }' },
|
|
34
35
|
buttons: { isFitting: isButtons, name: 'list of one to three labels' },
|
|
36
|
+
list: { isFitting: Array.isArray, name: 'list' },
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
// The options as the host takes them, so a misspelt or mistyped one fails here, saying which.
|
|
@@ -125,6 +127,46 @@ function checkedMenus(menus) {
|
|
|
125
127
|
return { menus };
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
// A tray item is { label, id, enabled, checked }, a submenu { label, items } or { type: 'separator' }: a tray's menu has
|
|
131
|
+
// no keys, and no standard item.
|
|
132
|
+
const TRAY = { tooltip: 'string', menu: 'list' };
|
|
133
|
+
const TRAY_ITEM = { label: 'string', id: 'string', enabled: 'boolean', checked: 'boolean', items: 'list', type: 'string' };
|
|
134
|
+
const NOTIFICATION = { title: 'string', body: 'string' };
|
|
135
|
+
|
|
136
|
+
function checkTrayItem(item, where) {
|
|
137
|
+
const refuse = (what) => {
|
|
138
|
+
throw new TypeError(`crossbind: tray.set's ${where} ${what}.`);
|
|
139
|
+
};
|
|
140
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) refuse('is no object');
|
|
141
|
+
for (const [key, value] of Object.entries(item)) {
|
|
142
|
+
if (!Object.hasOwn(TRAY_ITEM, key)) refuse(`takes ${Object.keys(TRAY_ITEM).join(', ')}; ${key} is none of them`);
|
|
143
|
+
const kind = TRAY_ITEM[key];
|
|
144
|
+
if (value !== undefined && !(kind === 'list' ? Array.isArray(value) : typeof value === kind)) refuse(`.${key} is no ${kind}`);
|
|
145
|
+
}
|
|
146
|
+
const others = Object.keys(item).filter((key) => key !== 'type' && item[key] !== undefined);
|
|
147
|
+
if (item.type !== undefined) {
|
|
148
|
+
if (item.type !== 'separator' || others.length > 0) refuse("is no separator, which is { type: 'separator' } alone");
|
|
149
|
+
} else if (!item.label) {
|
|
150
|
+
refuse('has no label');
|
|
151
|
+
} else if (item.items !== undefined) {
|
|
152
|
+
if (others.some((key) => key !== 'label' && key !== 'items')) refuse('is a menu, which takes a label and items alone');
|
|
153
|
+
// entries() reads a hole as undefined, which forEach skips.
|
|
154
|
+
for (const [index, child] of item.items.entries()) checkTrayItem(child, `${where}.items[${index}]`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function checkedTray(options) {
|
|
159
|
+
checked('tray.set', options, TRAY);
|
|
160
|
+
for (const [index, item] of (options.menu ?? []).entries()) checkTrayItem(item, `menu[${index}]`);
|
|
161
|
+
return options;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function checkedNotification(options) {
|
|
165
|
+
checked('notification.show', options, NOTIFICATION);
|
|
166
|
+
if (!options.title) throw new TypeError('crossbind: notification.show takes a title.');
|
|
167
|
+
return options;
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
export const dialog = {
|
|
129
171
|
// The paths of the files or folders the user picked, or null when they cancelled.
|
|
130
172
|
open: async (options = {}) => call('dialog.open', checked('dialog.open', options, OPEN)),
|
|
@@ -145,6 +187,9 @@ export const shell = {
|
|
|
145
187
|
openPath: async (path) => call('shell.openPath', checkedPath('shell.openPath', path)),
|
|
146
188
|
// A file, selected, in the system's file manager: one in the app's folders, or one a dialog picked during this run.
|
|
147
189
|
revealInFolder: async (path) => call('shell.revealInFolder', checkedPath('shell.revealInFolder', path)),
|
|
190
|
+
// 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
|
|
191
|
+
// them itself, or one a dialog picked during this run. Where the system has no trash, nothing moves.
|
|
192
|
+
trash: async (path) => call('shell.trash', checkedPath('shell.trash', path)),
|
|
148
193
|
};
|
|
149
194
|
|
|
150
195
|
export const app = {
|
|
@@ -152,6 +197,8 @@ export const app = {
|
|
|
152
197
|
paths: async () => call('app.paths', {}),
|
|
153
198
|
// Ends the app as closing its window does.
|
|
154
199
|
quit: async () => call('app.quit', {}),
|
|
200
|
+
// Ends the app as quit() does, then opens it again, with no link.
|
|
201
|
+
relaunch: async () => call('app.relaunch', {}),
|
|
155
202
|
// The links of app.urlSchemes that opened the app and have not reached the page yet, the one it was launched with
|
|
156
203
|
// among them; later ones come as the event open-url. Listening first, then asking, misses none.
|
|
157
204
|
launchUrls: async () => call('app.launchUrls', {}),
|
|
@@ -164,6 +211,20 @@ export const menu = {
|
|
|
164
211
|
set: async (menus) => call('menu.set', checkedMenus(menus)),
|
|
165
212
|
};
|
|
166
213
|
|
|
214
|
+
export const tray = {
|
|
215
|
+
// The app's icon in the macOS menu bar, the Windows notification area or a Linux desktop's status tray, with its
|
|
216
|
+
// tooltip and the menu a click opens. A click of an item with an id is the event menu, as one of menu.set's is, and a
|
|
217
|
+
// click of the icon the event tray where it opens no menu. Set again, it changes the icon that shows.
|
|
218
|
+
set: async (options = {}) => call('tray.set', checkedTray(options)),
|
|
219
|
+
// Takes the icon away.
|
|
220
|
+
remove: async () => call('tray.remove', {}),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const notification = {
|
|
224
|
+
// A notification of the system's, with a title and a body; a click on it is the event notification.
|
|
225
|
+
show: async (options) => call('notification.show', checkedNotification(options)),
|
|
226
|
+
};
|
|
227
|
+
|
|
167
228
|
export const events = {
|
|
168
229
|
// Calls listener with the value of each event of this name the app's native code emits from now on, until the
|
|
169
230
|
// function it returns is called.
|
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.18",
|
|
3
3
|
"registry": "ghcr.io/crossbind",
|
|
4
4
|
"toolchains": {
|
|
5
5
|
"rust": "1.99.1"
|
|
6
6
|
},
|
|
7
7
|
"images": {
|
|
8
8
|
"rust-sysroot": {
|
|
9
|
-
"index": "sha256:
|
|
9
|
+
"index": "sha256:31a213289c4dc1125b462ef006cc27b84ee7eca4e3ef0a93259bd698d56c7359",
|
|
10
10
|
"platforms": {
|
|
11
|
-
"linux/
|
|
12
|
-
"linux/
|
|
11
|
+
"linux/arm64": "sha256:e82b9a4b9661356238b264a9d5b6921d364cbc81c54ec8426349527144b91bbc",
|
|
12
|
+
"linux/amd64": "sha256:33c63cb3ed60a6d771f079a60c97dfa1f183f215d4087bb655ff4deca2366d9a"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"base": {
|
|
16
|
-
"index": "sha256:
|
|
16
|
+
"index": "sha256:ca5b17d648e797193d7f3c67635e1e73e56b58d02699a55ea61a35eb5ca8d0c8",
|
|
17
17
|
"platforms": {
|
|
18
|
-
"linux/arm64": "sha256:
|
|
19
|
-
"linux/amd64": "sha256:
|
|
18
|
+
"linux/arm64": "sha256:48666c55a2f943d8be029080fab985839bc50231b8556a00100bc3702a7bcc51",
|
|
19
|
+
"linux/amd64": "sha256:847f3c686dc8a846b05ee76d38a79b9ac68fb293ca014d56967ee12859849f1f"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"web": {
|
|
23
|
-
"index": "sha256:
|
|
23
|
+
"index": "sha256:72cdc6f5b41d946edc7ca8cd5e1bcf7314f03dfc71be9067994b38130f965711",
|
|
24
24
|
"platforms": {
|
|
25
|
-
"linux/arm64": "sha256:
|
|
26
|
-
"linux/amd64": "sha256:
|
|
25
|
+
"linux/arm64": "sha256:6d1756fa994e5d939ac5696ebe32012f56c95d0428a8bc7b3775d53acbff8735",
|
|
26
|
+
"linux/amd64": "sha256:14aa215c2157e485ce6d664e0077a3cf6938733855b8146e97830e48fda13875"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"android": {
|
|
30
|
-
"index": "sha256:
|
|
30
|
+
"index": "sha256:7cbd98594a1fb1f48fe156a21c6c5256cd535d2ffd152c1f80f2860d4dc5b6f0",
|
|
31
31
|
"platforms": {
|
|
32
|
-
"linux/amd64": "sha256:
|
|
32
|
+
"linux/amd64": "sha256:a6e99a0a66e038fd0e6d1154df6c7c58d68acd9def896b63f542d810d923bd87"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"android-app": {
|
|
36
|
-
"index": "sha256:
|
|
36
|
+
"index": "sha256:5cfd1df40ae28730d592f76ade7e08e628f0002d73979015308d50727f021f3e",
|
|
37
37
|
"platforms": {
|
|
38
|
-
"linux/amd64": "sha256:
|
|
38
|
+
"linux/amd64": "sha256:98d035dfaeb27095d0a4edce83299ebbb3ffa8e360a4cb925db7cd0b0b8a4666"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"linux": {
|
|
42
|
-
"index": "sha256:
|
|
42
|
+
"index": "sha256:9114bdc0e6b2c1b753d2ba154181566c320e9a901030a14c707f174b7e4849f5",
|
|
43
43
|
"platforms": {
|
|
44
|
-
"linux/arm64": "sha256:
|
|
45
|
-
"linux/amd64": "sha256:
|
|
44
|
+
"linux/arm64": "sha256:47e104db909dc78a995733d3d27db94e135f1044e1f0860bb201930a65273af0",
|
|
45
|
+
"linux/amd64": "sha256:cd13e6b3f5c5303a583da5267531a83839fe8cf7979890463fc45a914a6d5d3f"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"windows": {
|
|
49
|
-
"index": "sha256:
|
|
49
|
+
"index": "sha256:46e6da18313f4fd3b599fb2505383aa34f32ffe7ceb659426fa3fb71e6f1e8e7",
|
|
50
50
|
"platforms": {
|
|
51
|
-
"linux/arm64": "sha256:
|
|
52
|
-
"linux/amd64": "sha256:
|
|
51
|
+
"linux/arm64": "sha256:b6457367be5c6863f8d336ce426b9dba7fdfa192308876e4bc390d82a6e18474",
|
|
52
|
+
"linux/amd64": "sha256:fd5bdc1f34d64bb9aebae1e1d44fc380c1dd815bd6686349e428d4551732f6e0"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/bin.js
CHANGED
|
@@ -15,8 +15,13 @@ import buildWasiCommand from './actions/buildWasiCommand.js';
|
|
|
15
15
|
import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
|
|
16
16
|
import buildNativeCommand from './actions/buildNativeCommand.js';
|
|
17
17
|
import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
|
|
18
|
-
import { releaseSigning } from './actions/androidGradle.js';
|
|
19
18
|
import { UPDATER_KEY, newUpdaterKey } from './actions/appUpdate.js';
|
|
19
|
+
import {
|
|
20
|
+
makeUploadKey, releaseSigningEnv, showUploadKeyPassword, useUploadKey,
|
|
21
|
+
} from './actions/uploadKey.js';
|
|
22
|
+
import { publishAndroid } from './actions/publishAndroid.js';
|
|
23
|
+
import { publishIos } from './actions/publishIos.js';
|
|
24
|
+
import { iosDeviceOption } from './actions/iosDevice.js';
|
|
20
25
|
import buildExternal from './actions/buildExternal.js';
|
|
21
26
|
import buildPackageTypes from './actions/buildTypes.js';
|
|
22
27
|
import buildLib from './actions/buildLib.js';
|
|
@@ -116,30 +121,51 @@ function projectVite() {
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
// The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
|
|
125
|
+
// to crossbind alone reaches Vite too, which ends through its exit hooks; Ctrl-C reaches both. Once Vite has ended,
|
|
126
|
+
// signals stop crossbind again, as at the questions a publish asks after its build.
|
|
127
|
+
function runVite(args, env) {
|
|
128
|
+
const vite = spawn(process.execPath, [projectVite(), ...args], { stdio: 'inherit', cwd: state.config.paths.project, env });
|
|
129
|
+
const forwards = ['SIGINT', 'SIGTERM', 'SIGHUP'].map((signal) => [signal, () => vite.kill(signal)]);
|
|
130
|
+
forwards.forEach(([signal, forward]) => process.prependListener(signal, forward));
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
vite.on('exit', (code, signal) => {
|
|
133
|
+
forwards.forEach(([name, forward]) => process.off(name, forward));
|
|
134
|
+
resolve(code ?? 128 + os.constants.signals[signal]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertAppProject() {
|
|
140
|
+
if (state.config.target.runtimeEnv !== 'app') {
|
|
141
|
+
throw new Error("crossbind: crossbind app builds an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
const commandApp = program.command('app').description('build the project as an app, or open it on the dev server');
|
|
120
146
|
[
|
|
121
147
|
['dev', 'open the app on the dev server, and open it anew as its native code changes'],
|
|
122
148
|
['build', 'build the app with its page: for Android also its APK, and with an upload key its bundle'],
|
|
123
|
-
].forEach(([name, description]) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
env:
|
|
149
|
+
].forEach(([name, description]) => {
|
|
150
|
+
const appCommand = commandApp.command(name).description(description)
|
|
151
|
+
.addOption(new Option('-p, --platform <platform>', "the app's platform (default: CROSSBIND_APP_PLATFORM, else this machine's)").choices(APP_PLATFORMS));
|
|
152
|
+
if (name === 'dev') {
|
|
153
|
+
appCommand.option('--device [name]', 'open an iOS app on a connected iPhone or iPad, the one named or else the only one, rather than in the Simulator');
|
|
154
|
+
}
|
|
155
|
+
appCommand
|
|
156
|
+
// Vite's own options, as --port or --mode, go on to Vite.
|
|
157
|
+
.allowUnknownOption()
|
|
158
|
+
.allowExcessArguments()
|
|
159
|
+
.action(async ({ platform, device }, command) => {
|
|
160
|
+
assertAppProject();
|
|
161
|
+
// An Android build checks its upload key before the native build, not after it.
|
|
162
|
+
const isAndroidBuild = name === 'build' && (platform ?? process.env.CROSSBIND_APP_PLATFORM) === 'android';
|
|
163
|
+
const { env: signing } = isAndroidBuild ? await releaseSigningEnv() : { env: {} };
|
|
164
|
+
const onDevice = device === undefined ? {} : { CROSSBIND_APP_DEVICE: iosDeviceOption(device, platform ?? process.env.CROSSBIND_APP_PLATFORM) };
|
|
165
|
+
const env = { ...process.env, ...signing, ...(platform ? { CROSSBIND_APP_PLATFORM: platform } : {}), ...onDevice };
|
|
166
|
+
process.exit(await runVite([...(name === 'build' ? ['build'] : []), ...command.args], env));
|
|
138
167
|
});
|
|
139
|
-
|
|
140
|
-
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => process.prependListener(signal, () => vite.kill(signal)));
|
|
141
|
-
vite.on('exit', (code, signal) => process.exit(code ?? 128 + os.constants.signals[signal]));
|
|
142
|
-
}));
|
|
168
|
+
});
|
|
143
169
|
|
|
144
170
|
commandApp.command('updater-key')
|
|
145
171
|
.description("make the key pair that signs a desktop app's updates")
|
|
@@ -149,6 +175,41 @@ commandApp.command('updater-key')
|
|
|
149
175
|
console.log(`crossbind: ${UPDATER_KEY}, kept secret where the app is built: ${privateKey}`);
|
|
150
176
|
});
|
|
151
177
|
|
|
178
|
+
commandApp.command('upload-key')
|
|
179
|
+
.description("make this machine's upload key, which signs an Android app's bundle for Google Play")
|
|
180
|
+
.option('--use <keystore>', 'make a key you have the upload key instead, once it opens with its password')
|
|
181
|
+
.option('--alias <alias>', 'the alias of the key in the keystore --use names', 'upload')
|
|
182
|
+
.option('--no-keychain', 'keep the password in no keychain, so builds ask for it')
|
|
183
|
+
.option('--show-password', 'print the password the keychain keeps, for another machine or CI')
|
|
184
|
+
.action(async ({ use, alias, keychain, showPassword }) => {
|
|
185
|
+
if (showPassword) showUploadKeyPassword();
|
|
186
|
+
else if (use) await useUploadKey(use, { alias, isKeychain: keychain });
|
|
187
|
+
else await makeUploadKey({ isKeychain: keychain });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
commandApp.command('publish')
|
|
191
|
+
.description('build the app and send it to its store: an Android bundle to a Google Play track, an iOS app to App Store Connect for TestFlight')
|
|
192
|
+
.addOption(new Option('-p, --platform <platform>', "the store's platform (default: CROSSBIND_APP_PLATFORM)").choices(['android', 'ios']))
|
|
193
|
+
.option('--track <track>', 'the Google Play track the release goes on: internal, alpha, beta, production or one of your own', 'internal')
|
|
194
|
+
.option('--draft', 'leave the Google Play release a draft, which Play Console rolls out')
|
|
195
|
+
// Vite's own options, as --mode, go on to Vite.
|
|
196
|
+
.allowUnknownOption()
|
|
197
|
+
.allowExcessArguments()
|
|
198
|
+
.action(async ({ platform, track, draft }, command) => {
|
|
199
|
+
assertAppProject();
|
|
200
|
+
const store = platform ?? process.env.CROSSBIND_APP_PLATFORM;
|
|
201
|
+
if (store === 'android') {
|
|
202
|
+
await publishAndroid({ track, status: draft ? 'draft' : 'completed', viteArgs: command.args }, { runVite });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (store !== 'ios') throw new Error('crossbind: crossbind app publish takes -p android or -p ios, the platform of the store it sends the app to.');
|
|
206
|
+
const playOptions = ['track', 'draft'].filter((option) => command.getOptionValueSource(option) === 'cli');
|
|
207
|
+
if (playOptions.length > 0) {
|
|
208
|
+
throw new Error(`crossbind: ${playOptions.map((option) => `--${option}`).join(' and ')} ${playOptions.length > 1 ? 'are' : 'is'} Google Play's; App Store Connect hands every iOS upload to TestFlight.`);
|
|
209
|
+
}
|
|
210
|
+
await publishIos({ viteArgs: command.args }, { runVite });
|
|
211
|
+
});
|
|
212
|
+
|
|
152
213
|
program.command('licenses')
|
|
153
214
|
.description('list bundled native dependencies with SPDX license, versions and source URL')
|
|
154
215
|
.option('--notices [file]', 'write a THIRD-PARTY-NOTICES markdown file (default: THIRD-PARTY-NOTICES.md)')
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,9 @@ export {
|
|
|
8
8
|
} from './actions/buildApp.js';
|
|
9
9
|
export { checkInstaller } from './actions/appInstaller.js';
|
|
10
10
|
export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
|
|
11
|
+
export {
|
|
12
|
+
default as openOnIosDevice, buildForIosDevice, connectedIosDevice, iosDeviceAddress, iosDevServer,
|
|
13
|
+
} from './actions/iosDevice.js';
|
|
11
14
|
export { default as openOnDevice, adbPath } from './actions/androidDevice.js';
|
|
12
15
|
export { buildApk } from './actions/androidGradle.js';
|
|
13
16
|
export { default as isSourceNewer } from './actions/isSourceNewer.js';
|
package/src/program.js
CHANGED
|
@@ -22,7 +22,9 @@ export const handleFatal = (e) => {
|
|
|
22
22
|
if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
|
|
23
23
|
console.error(message);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// Writes to a pipe are asynchronous, so exiting at once would cut what is still queued, such as the failed
|
|
26
|
+
// command's output run.js printed: exit after both streams have written it.
|
|
27
|
+
process.stdout.write('', () => process.stderr.write('', () => process.exit(1)));
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
export const createProgram = () => new Command()
|
|
@@ -12,6 +12,10 @@ export function compilesAsApp(target) {
|
|
|
12
12
|
return state.config.target?.runtimeEnv === 'app' && runtimeEnvsOf(target.platform).includes('app');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// What an app's own code compiles with: CROSSBIND_APP, and on iOS its debug information, from which the archive's dSYM
|
|
16
|
+
// gives App Store Connect its files and lines; Android's NDK keeps that by default.
|
|
17
|
+
export const appCompileOptions = (target) => (compilesAsApp(target) ? ['-DCROSSBIND_APP', ...(target.platform === 'ios' ? ['-g'] : [])] : []);
|
|
18
|
+
|
|
15
19
|
export function appApiHeader() {
|
|
16
20
|
const header = `${resolveAppHostRoot()}/cpp/include/crossbind/app.h`;
|
|
17
21
|
if (!fs.existsSync(header)) {
|