crossbind 2.0.0-beta.67 → 2.0.0-beta.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -24
- package/package.json +69 -59
- package/src/actions/addExtension.js +191 -0
- package/src/actions/androidGradle.js +69 -25
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/appleSigning.js +32 -14
- package/src/actions/buildApp.js +229 -67
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createInterface.js +17 -8
- package/src/actions/createLib.js +5 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getCmakeParameters.js +2 -3
- package/src/actions/gradleProject.js +199 -16
- package/src/actions/iosDevice.js +200 -0
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +83 -34
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/publish.js +220 -0
- package/src/actions/uploadKey.js +134 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/xcodeProject.js +63 -17
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +126 -133
- package/src/app/index.js +126 -188
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +79 -26
- package/src/index.js +5 -1
- package/src/program.js +35 -4
- package/src/state/index.js +2 -1
- package/src/state/loadConfig.js +16 -5
- package/src/utils/appApiHeader.js +4 -0
- package/src/utils/appExtensions.js +761 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +144 -95
- package/src/utils/appTargets.js +168 -0
- package/src/utils/ask.js +38 -0
- package/src/utils/bridgeTools.js +66 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +181 -0
- package/src/utils/extensionSteps.js +135 -0
- package/src/utils/hermes.js +24 -0
- package/src/utils/keychain.js +54 -0
- package/src/utils/openUrl.js +12 -0
- package/src/utils/password.js +34 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/requiredReasonApis.js +77 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/secretFile.js +23 -0
- package/src/utils/systemConfig.js +19 -0
- package/src/utils/systemKeys.js +16 -0
- package/vitest.config.js +6 -1
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
|
@@ -3,12 +3,15 @@ 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 { mergedPlist } from '../utils/appExtensions.js';
|
|
7
|
+
import { appLinkParts, iosDocumentTypes, iosShortcutItems } from '../utils/appLaunches.js';
|
|
8
|
+
import { privacyManifest, writeTargets } from './xcodeTargets.js';
|
|
6
9
|
|
|
7
10
|
// The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
|
|
8
11
|
export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
|
|
9
12
|
export const HERMES_SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64_x86_64-simulator' };
|
|
10
13
|
// What the host's own code needs from the system: Xcode adds it only for the sources it compiles itself.
|
|
11
|
-
const SYSTEM_LIBS = ['-framework', 'UIKit', '-framework', 'WebKit', '-framework', 'CoreGraphics', '-lc++'];
|
|
14
|
+
const SYSTEM_LIBS = ['-framework', 'UIKit', '-framework', 'WebKit', '-framework', 'CoreGraphics', '-framework', 'Security', '-framework', 'UserNotifications', '-lc++'];
|
|
12
15
|
|
|
13
16
|
const MAIN = `// crossbind writes this project anew on every build: the app comes prebuilt in lib/, and its files are in app/.
|
|
14
17
|
int crossbind_app_main(int argc, char *argv[]);
|
|
@@ -18,7 +21,7 @@ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
|
|
|
18
21
|
|
|
19
22
|
const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
|
|
20
23
|
'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
|
|
21
|
-
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild'];
|
|
24
|
+
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild', 'privacy', 'privacyBuild', 'embedTargets'];
|
|
22
25
|
// Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
|
|
23
26
|
const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
|
|
24
27
|
|
|
@@ -39,7 +42,14 @@ const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationL
|
|
|
39
42
|
// The color set of app.window.backgroundColor, which the launch screen shows.
|
|
40
43
|
const LAUNCH_COLOR = 'LaunchBackground';
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
// The entitlement of the domains whose sites' links iOS hands the app, once each site's apple-app-site-association names
|
|
46
|
+
// it, and the one of the App Groups it shares with the targets of its extensions'.
|
|
47
|
+
const ASSOCIATED_DOMAINS = 'com.apple.developer.associated-domains';
|
|
48
|
+
const APP_GROUPS = 'com.apple.security.application-groups';
|
|
49
|
+
|
|
50
|
+
// The app's Info.plist, with the keys its extensions give, and the App Group its native code reads where the targets of
|
|
51
|
+
// its extensions' share one.
|
|
52
|
+
const infoPlist = (name, urlSchemes, fileTypes, activityTypes, shortcutItems, hasBackground, usage, encryption, multipleWindows, appGroup, plist) => xmlPlist(mergedPlist({
|
|
43
53
|
CFBundleDevelopmentRegion: 'en',
|
|
44
54
|
CFBundleDisplayName: name,
|
|
45
55
|
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
@@ -50,16 +60,22 @@ const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
|
50
60
|
CFBundleShortVersionString: '$(MARKETING_VERSION)',
|
|
51
61
|
...(urlSchemes.length > 0 ? { CFBundleURLTypes: [{ CFBundleURLName: '$(PRODUCT_BUNDLE_IDENTIFIER)', CFBundleURLSchemes: urlSchemes }] } : {}),
|
|
52
62
|
CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
63
|
+
...(encryption === null ? {} : { ITSAppUsesNonExemptEncryption: encryption }),
|
|
53
64
|
LSRequiresIPhoneOS: true,
|
|
54
|
-
UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes:
|
|
65
|
+
UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: multipleWindows },
|
|
55
66
|
UILaunchScreen: hasBackground ? { UIColorName: LAUNCH_COLOR } : {},
|
|
56
67
|
UISupportedInterfaceOrientations: ORIENTATIONS,
|
|
57
68
|
'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
|
|
69
|
+
...iosDocumentTypes(fileTypes, '$(PRODUCT_BUNDLE_IDENTIFIER)'),
|
|
70
|
+
...(activityTypes.length > 0 ? { NSUserActivityTypes: activityTypes } : {}),
|
|
71
|
+
...(shortcutItems.length > 0 ? { UIApplicationShortcutItems: iosShortcutItems(shortcutItems) } : {}),
|
|
58
72
|
// The reasons of app.permissions, without which iOS lets the app use none of those devices.
|
|
59
73
|
...usage,
|
|
60
|
-
})
|
|
74
|
+
...(appGroup ? { CrossbindAppGroup: appGroup } : {}),
|
|
75
|
+
}, plist));
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
// The project's objects: the app's target, and the targets its extensions add, which the app depends on and embeds.
|
|
78
|
+
function objects(name, targetSettings, hasAssets, hasPrivacy, added) {
|
|
63
79
|
const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
|
|
64
80
|
const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
|
|
65
81
|
const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
|
|
@@ -71,8 +87,15 @@ function objects(name, targetSettings, hasAssets) {
|
|
|
71
87
|
[ID.assets]: file('Assets.xcassets', 'folder.assetcatalog'),
|
|
72
88
|
[ID.assetsBuild]: { isa: 'PBXBuildFile', fileRef: ID.assets },
|
|
73
89
|
} : {};
|
|
90
|
+
const privacy = hasPrivacy ? {
|
|
91
|
+
[ID.privacy]: file('PrivacyInfo.xcprivacy', 'text.xml'),
|
|
92
|
+
[ID.privacyBuild]: { isa: 'PBXBuildFile', fileRef: ID.privacy },
|
|
93
|
+
} : {};
|
|
94
|
+
const hasTargets = added.targets.length > 0;
|
|
74
95
|
return {
|
|
75
96
|
...assets,
|
|
97
|
+
...privacy,
|
|
98
|
+
...added.objects,
|
|
76
99
|
[ID.main]: file('main.m', 'sourcecode.c.objc'),
|
|
77
100
|
[ID.info]: file('Info.plist', 'text.plist.xml'),
|
|
78
101
|
[ID.app]: file('app', 'folder'),
|
|
@@ -86,18 +109,21 @@ function objects(name, targetSettings, hasAssets) {
|
|
|
86
109
|
[ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
|
|
87
110
|
[ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
|
|
88
111
|
[ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
|
|
89
|
-
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : [])]),
|
|
112
|
+
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : []), ...(hasPrivacy ? [ID.privacyBuild] : [])]),
|
|
90
113
|
[ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
|
|
114
|
+
...(hasTargets ? { [ID.embedTargets]: phase('PBXCopyFilesBuildPhase', added.embedded, { dstPath: '', dstSubfolderSpec: 13, name: 'Embed Foundation Extensions' }) } : {}),
|
|
91
115
|
[ID.mainGroup]: {
|
|
92
|
-
isa: 'PBXGroup',
|
|
116
|
+
isa: 'PBXGroup',
|
|
117
|
+
children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ...(hasPrivacy ? [ID.privacy] : []), ID.hermes, ...added.groups, ID.products],
|
|
118
|
+
sourceTree: '<group>',
|
|
93
119
|
},
|
|
94
|
-
[ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
|
|
120
|
+
[ID.products]: { isa: 'PBXGroup', children: [ID.product, ...added.products], name: 'Products', sourceTree: '<group>' },
|
|
95
121
|
[ID.target]: {
|
|
96
122
|
isa: 'PBXNativeTarget',
|
|
97
123
|
buildConfigurationList: ID.targetConfigs,
|
|
98
|
-
buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed],
|
|
124
|
+
buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed, ...(hasTargets ? [ID.embedTargets] : [])],
|
|
99
125
|
buildRules: [],
|
|
100
|
-
dependencies:
|
|
126
|
+
dependencies: added.dependencies,
|
|
101
127
|
name,
|
|
102
128
|
productName: name,
|
|
103
129
|
productReference: ID.product,
|
|
@@ -114,7 +140,7 @@ function objects(name, targetSettings, hasAssets) {
|
|
|
114
140
|
productRefGroup: ID.products,
|
|
115
141
|
projectDirPath: '',
|
|
116
142
|
projectRoot: '',
|
|
117
|
-
targets: [ID.target],
|
|
143
|
+
targets: [ID.target, ...added.targets],
|
|
118
144
|
},
|
|
119
145
|
[ID.projectDebug]: configuration('Debug', PROJECT_SETTINGS),
|
|
120
146
|
[ID.projectRelease]: configuration('Release', PROJECT_SETTINGS),
|
|
@@ -145,10 +171,24 @@ function writeAssets(catalog, icon, background) {
|
|
|
145
171
|
|
|
146
172
|
// An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
|
|
147
173
|
// 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
|
-
// Simulator or on a device, and makes every size of its icon from the one PNG given.
|
|
149
|
-
|
|
174
|
+
// Simulator or on a device, with the entitlements given, and makes every size of its icon from the one PNG given. The
|
|
175
|
+
// targets of Apple's app extensions its extensions add build with it, the App Group in the app's entitlements as well
|
|
176
|
+
// where one of them asks for it.
|
|
177
|
+
export default function writeXcodeProject(to, {
|
|
178
|
+
parts, identity, label, version, build = version, icon = null, urlSchemes = [], fileTypes = [], links = [], activityTypes = [], shortcutItems = [],
|
|
179
|
+
background = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [], appGroup = null,
|
|
180
|
+
}) {
|
|
150
181
|
const { name } = state.config.general;
|
|
151
182
|
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.`);
|
|
183
|
+
// Xcode keeps what it builds of each target by the target's name, which a Mac's file system takes in either case.
|
|
184
|
+
const named = targets.find((target) => target.name.toLowerCase() === name.toLowerCase());
|
|
185
|
+
if (named) throw new Error(`crossbind: the extension ${named.owner} names an iOS target ${named.name}, as the app itself is named: Xcode builds no two targets of one name.`);
|
|
186
|
+
// The extensions' keys merge before the project goes, which a conflict between them then leaves as it was.
|
|
187
|
+
const group = appGroup && targets.some((target) => target.appGroup) ? appGroup : null;
|
|
188
|
+
const info = infoPlist(label, urlSchemes, fileTypes, activityTypes, shortcutItems, Boolean(background), usage, encryption, multipleWindows, group, plist);
|
|
189
|
+
const domains = links.map((link) => `applinks:${appLinkParts(link).host}`);
|
|
190
|
+
const joinedTo = (given, key, values) => (values.length > 0 ? { ...given, [key]: [...new Set([...(given[key] ?? []), ...values])] } : given);
|
|
191
|
+
const signed = joinedTo(joinedTo(entitlements, ASSOCIATED_DOMAINS, domains), APP_GROUPS, group ? [group] : []);
|
|
152
192
|
fs.rmSync(to, { recursive: true, force: true });
|
|
153
193
|
fs.mkdirSync(`${to}/${name}.xcodeproj`, { recursive: true });
|
|
154
194
|
const linkFlags = {};
|
|
@@ -165,22 +205,28 @@ export default function writeXcodeProject(to, { parts, identity, label, version,
|
|
|
165
205
|
if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
|
|
166
206
|
|
|
167
207
|
fs.writeFileSync(`${to}/main.m`, MAIN);
|
|
168
|
-
fs.writeFileSync(`${to}/Info.plist`,
|
|
208
|
+
fs.writeFileSync(`${to}/Info.plist`, info);
|
|
169
209
|
writeAssets(`${to}/Assets.xcassets`, icon, background);
|
|
210
|
+
const hasPrivacy = Object.keys(privacy).length > 0;
|
|
211
|
+
if (hasPrivacy) fs.writeFileSync(`${to}/PrivacyInfo.xcprivacy`, privacyManifest(privacy));
|
|
212
|
+
const hasEntitlements = Object.keys(signed).length > 0;
|
|
213
|
+
if (hasEntitlements) fs.writeFileSync(`${to}/${name}.entitlements`, xmlPlist(signed));
|
|
170
214
|
const targetSettings = {
|
|
171
215
|
...(icon ? { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon' } : {}),
|
|
172
216
|
PRODUCT_NAME: name,
|
|
173
217
|
PRODUCT_BUNDLE_IDENTIFIER: identity.id,
|
|
174
218
|
DEVELOPMENT_TEAM: identity.team,
|
|
175
219
|
CODE_SIGN_STYLE: 'Automatic',
|
|
220
|
+
...(hasEntitlements ? { CODE_SIGN_ENTITLEMENTS: `${name}.entitlements` } : {}),
|
|
176
221
|
INFOPLIST_FILE: 'Info.plist',
|
|
177
222
|
MARKETING_VERSION: version,
|
|
178
|
-
CURRENT_PROJECT_VERSION:
|
|
223
|
+
CURRENT_PROJECT_VERSION: build,
|
|
179
224
|
LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
|
|
180
225
|
...linkFlags,
|
|
181
226
|
};
|
|
227
|
+
const added = writeTargets(to, targets, { project: ID.project, identity, label, version, build, appGroup });
|
|
182
228
|
fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
|
|
183
|
-
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background)), rootObject: ID.project,
|
|
229
|
+
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background), hasPrivacy, added), rootObject: ID.project,
|
|
184
230
|
}));
|
|
185
231
|
return to;
|
|
186
232
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { xmlPlist } from '../utils/plist.js';
|
|
4
|
+
import { PRIVACY_CATEGORIES } from '../utils/appOptions.js';
|
|
5
|
+
import { isHeader, targetFrameworks, targetModule, targetPlist, widgetsWithoutBackground } from '../utils/appTargets.js';
|
|
6
|
+
|
|
7
|
+
const APP_GROUPS = 'com.apple.security.application-groups';
|
|
8
|
+
// The kind of each file Xcode compiles or copies into a target, by its ending: a folder is copied as it is, and an asset
|
|
9
|
+
// catalog compiled.
|
|
10
|
+
const FILE_TYPES = {
|
|
11
|
+
'.swift': 'sourcecode.swift', '.m': 'sourcecode.c.objc', '.mm': 'sourcecode.cpp.objcpp', '.c': 'sourcecode.c.c', '.cpp': 'sourcecode.cpp.cpp',
|
|
12
|
+
'.cc': 'sourcecode.cpp.cpp', '.cxx': 'sourcecode.cpp.cpp', '.h': 'sourcecode.c.h', '.hpp': 'sourcecode.cpp.h', '.xcassets': 'folder.assetcatalog',
|
|
13
|
+
'.storyboard': 'file.storyboard', '.xib': 'file.xib', '.strings': 'text.plist.strings', '.xcstrings': 'text.json.xcstrings', '.png': 'image.png',
|
|
14
|
+
'.json': 'text.json',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// The privacy manifest of these reasons for Apple's required reason API, which App Store Connect asks of each bundle that
|
|
18
|
+
// calls one.
|
|
19
|
+
export const privacyManifest = (privacy) => xmlPlist({
|
|
20
|
+
NSPrivacyAccessedAPITypes: Object.entries(privacy).map(([name, reasons]) => ({
|
|
21
|
+
NSPrivacyAccessedAPIType: PRIVACY_CATEGORIES[name].type,
|
|
22
|
+
NSPrivacyAccessedAPITypeReasons: reasons,
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Xcode names its objects by 24 hex digits: each target's go by its place among them, apart from the app's own.
|
|
27
|
+
const idOf = (target, index) => `CE${target.toString(16).toUpperCase().padStart(4, '0')}${index.toString(16).toUpperCase().padStart(18, '0')}`;
|
|
28
|
+
|
|
29
|
+
// Writes a target's folder beside the app's project, targets/<name>/: its sources and resources in files/, where they are
|
|
30
|
+
// in their extension, its Info.plist, and its entitlements, the App Group among them where it asks for it, and its
|
|
31
|
+
// privacy manifest where it has them.
|
|
32
|
+
function writeTargetFiles(to, target, { label, appGroup }) {
|
|
33
|
+
const dir = `targets/${target.name}`;
|
|
34
|
+
fs.mkdirSync(`${to}/${dir}/files`, { recursive: true });
|
|
35
|
+
[...target.sources, ...(target.resources ?? [])].forEach((file) => fs.cpSync(path.join(target.root, file), `${to}/${dir}/files/${file}`, { recursive: true }));
|
|
36
|
+
fs.writeFileSync(`${to}/${dir}/Info.plist`, xmlPlist(targetPlist(target, label, appGroup)));
|
|
37
|
+
const own = target.entitlements ?? {};
|
|
38
|
+
const entitlements = target.appGroup ? { ...own, [APP_GROUPS]: [...new Set([...(own[APP_GROUPS] ?? []), appGroup])] } : own;
|
|
39
|
+
const hasEntitlements = Object.keys(entitlements).length > 0;
|
|
40
|
+
if (hasEntitlements) fs.writeFileSync(`${to}/${dir}/${target.name}.entitlements`, xmlPlist(entitlements));
|
|
41
|
+
const hasPrivacy = Object.keys(target.privacy ?? {}).length > 0;
|
|
42
|
+
if (hasPrivacy) fs.writeFileSync(`${to}/${dir}/PrivacyInfo.xcprivacy`, privacyManifest(target.privacy));
|
|
43
|
+
return { dir, hasEntitlements, hasPrivacy };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const linkedFrameworks = (frameworks) => (frameworks.length > 0 ? { OTHER_LDFLAGS: ['$(inherited)', ...frameworks.flatMap((name) => ['-framework', name])] } : {});
|
|
47
|
+
|
|
48
|
+
// A target's build settings: its bundle, of the app's id and its name, signed as the app is, with the app's version and
|
|
49
|
+
// build, and its own deployment target and frameworks.
|
|
50
|
+
const targetSettings = (target, { dir, hasEntitlements }, { identity, version, build }) => ({
|
|
51
|
+
PRODUCT_NAME: target.name,
|
|
52
|
+
PRODUCT_MODULE_NAME: targetModule(target.name),
|
|
53
|
+
PRODUCT_BUNDLE_IDENTIFIER: `${identity.id}.${target.name}`,
|
|
54
|
+
DEVELOPMENT_TEAM: identity.team,
|
|
55
|
+
CODE_SIGN_STYLE: 'Automatic',
|
|
56
|
+
...(hasEntitlements ? { CODE_SIGN_ENTITLEMENTS: `${dir}/${target.name}.entitlements` } : {}),
|
|
57
|
+
INFOPLIST_FILE: `${dir}/Info.plist`,
|
|
58
|
+
MARKETING_VERSION: version,
|
|
59
|
+
CURRENT_PROJECT_VERSION: build,
|
|
60
|
+
SKIP_INSTALL: 'YES',
|
|
61
|
+
APPLICATION_EXTENSION_API_ONLY: 'YES',
|
|
62
|
+
// Objective-C's @import, and the frameworks it links of itself.
|
|
63
|
+
CLANG_ENABLE_MODULES: 'YES',
|
|
64
|
+
LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks', '@executable_path/../../Frameworks'],
|
|
65
|
+
...(target.sources.some((file) => file.endsWith('.swift')) ? { SWIFT_VERSION: '5.0' } : {}),
|
|
66
|
+
...(target.deploymentTarget ? { IPHONEOS_DEPLOYMENT_TARGET: target.deploymentTarget } : {}),
|
|
67
|
+
...linkedFrameworks(targetFrameworks(target)),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// The project's objects of one target, by ids of its place among them: its files, build phases, configurations and the
|
|
71
|
+
// target, with what the app's target takes of it, a dependency on it and the copy of its product into the app's PlugIns.
|
|
72
|
+
function targetObjects(target, place, written, options) {
|
|
73
|
+
const objects = {};
|
|
74
|
+
let count = 0;
|
|
75
|
+
const add = (object) => {
|
|
76
|
+
count += 1;
|
|
77
|
+
objects[idOf(place, count)] = object;
|
|
78
|
+
return idOf(place, count);
|
|
79
|
+
};
|
|
80
|
+
const reference = (file, type) => add({ isa: 'PBXFileReference', lastKnownFileType: type, path: file, sourceTree: '<group>' });
|
|
81
|
+
const typeOf = (file) => FILE_TYPES[path.extname(file)] ?? (fs.statSync(path.join(target.root, file)).isDirectory() ? 'folder' : 'file');
|
|
82
|
+
const phase = (isa, files) => add({ isa, buildActionMask: 2147483647, files: files.map((fileRef) => add({ isa: 'PBXBuildFile', fileRef })), runOnlyForDeploymentPostprocessing: 0 });
|
|
83
|
+
// Its headers are in its group, which its sources import from beside them, and compile with them.
|
|
84
|
+
const headers = target.sources.filter(isHeader).map((file) => reference(`files/${file}`, typeOf(file)));
|
|
85
|
+
const sources = target.sources.filter((file) => !isHeader(file)).map((file) => reference(`files/${file}`, typeOf(file)));
|
|
86
|
+
const resources = [...(target.resources ?? []).map((file) => reference(`files/${file}`, typeOf(file))),
|
|
87
|
+
...(written.hasPrivacy ? [reference('PrivacyInfo.xcprivacy', 'text.xml')] : [])];
|
|
88
|
+
const info = reference('Info.plist', 'text.plist.xml');
|
|
89
|
+
const signed = written.hasEntitlements ? [reference(`${target.name}.entitlements`, 'text.plist.entitlements')] : [];
|
|
90
|
+
const product = add({ isa: 'PBXFileReference', explicitFileType: 'wrapper.app-extension', includeInIndex: 0, path: `${target.name}.appex`, sourceTree: 'BUILT_PRODUCTS_DIR' });
|
|
91
|
+
const group = add({ isa: 'PBXGroup', children: [info, ...signed, ...headers, ...sources, ...resources], path: written.dir, sourceTree: '<group>' });
|
|
92
|
+
const phases = [phase('PBXSourcesBuildPhase', sources), phase('PBXFrameworksBuildPhase', []), phase('PBXResourcesBuildPhase', resources)];
|
|
93
|
+
const buildSettings = targetSettings(target, written, options);
|
|
94
|
+
const configurations = ['Debug', 'Release'].map((name) => add({ isa: 'XCBuildConfiguration', buildSettings, name }));
|
|
95
|
+
const list = add({ isa: 'XCConfigurationList', buildConfigurations: configurations, defaultConfigurationIsVisible: 0, defaultConfigurationName: 'Release' });
|
|
96
|
+
const native = add({
|
|
97
|
+
isa: 'PBXNativeTarget', buildConfigurationList: list, buildPhases: phases, buildRules: [], dependencies: [], name: target.name,
|
|
98
|
+
productName: target.name, productReference: product, productType: 'com.apple.product-type.app-extension',
|
|
99
|
+
});
|
|
100
|
+
const proxy = add({ isa: 'PBXContainerItemProxy', containerPortal: options.project, proxyType: 1, remoteGlobalIDString: native, remoteInfo: target.name });
|
|
101
|
+
const dependency = add({ isa: 'PBXTargetDependency', target: native, targetProxy: proxy });
|
|
102
|
+
const embedded = add({ isa: 'PBXBuildFile', fileRef: product, settings: { ATTRIBUTES: ['RemoveHeadersOnCopy'] } });
|
|
103
|
+
return { objects, group, product, native, dependency, embedded };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Writes the folder of each target of Apple's app extensions the app's extensions add, and gives the project's objects
|
|
107
|
+
// for them: each a target whose bundle is the app's id and its name, which the app builds first and embeds in its PlugIns.
|
|
108
|
+
export function writeTargets(to, targets, options) {
|
|
109
|
+
widgetsWithoutBackground(targets, (target, file) => fs.readFileSync(path.join(target.root, file), 'utf8')).forEach((name) => {
|
|
110
|
+
console.warn(`crossbind: the widget target ${name} names no containerBackground in its Swift, which iOS 17 asks of a widget's view: a debug build `
|
|
111
|
+
+ 'draws "Please adopt containerBackground API" in its place. Its view gives its background with .containerBackground(.background, for: .widget).');
|
|
112
|
+
});
|
|
113
|
+
const made = targets.map((target, place) => targetObjects(target, place, writeTargetFiles(to, target, options), options));
|
|
114
|
+
return {
|
|
115
|
+
objects: Object.assign({}, ...made.map(({ objects }) => objects)),
|
|
116
|
+
groups: made.map(({ group }) => group),
|
|
117
|
+
products: made.map(({ product }) => product),
|
|
118
|
+
dependencies: made.map(({ dependency }) => dependency),
|
|
119
|
+
targets: made.map(({ native }) => native),
|
|
120
|
+
embedded: made.map(({ embedded }) => embedded),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { EXEC_MAX_BUFFER } from './run.js';
|
|
4
|
+
|
|
5
|
+
const DEVELOPER_ACCOUNT = 'https://developer.apple.com/account';
|
|
6
|
+
// The lines of a failed xcodebuild's output that say why; its log has the rest.
|
|
7
|
+
const REASON_LINES = 15;
|
|
8
|
+
|
|
9
|
+
// What a failed build, archive or upload means where xcodebuild's own words leave it unclear: what the user mends in
|
|
10
|
+
// Xcode or on Apple's sites, and the page where. A cause may need another build, as one in what was built does.
|
|
11
|
+
const CAUSES = [
|
|
12
|
+
{
|
|
13
|
+
pattern: /login details for account .* were rejected|No Accounts?\b|No Account for Team/i,
|
|
14
|
+
hint: ({ team }) => `Xcode is signed in to no account of team ${team}, or its sign-in has lapsed: sign in again in Xcode, under Settings › Accounts.`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /PLA Update available|Program License Agreement/i,
|
|
18
|
+
hint: () => `Apple has a new Program License Agreement, which the team's Account Holder accepts at ${DEVELOPER_ACCOUNT}.`,
|
|
19
|
+
page: DEVELOPER_ACCOUNT,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
pattern: /Cloud signing permission error/i,
|
|
23
|
+
hint: () => "the API key may not sign in the cloud, which takes its Admin role: use an Admin key, make an Apple Distribution certificate once in Xcode, under Settings › Accounts › Manage Certificates, or unset CROSSBIND_APPLE_API_KEY to upload with Xcode's account.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /cannot be registered to your development team because it is not available/i,
|
|
27
|
+
hint: ({ id }) => `another team registered the bundle id ${id} with Apple first: set app.id in crossbind.config.js to an id of your own, as one under a domain you own.`,
|
|
28
|
+
// The id is in what was built, so the same step tried again fails the same way.
|
|
29
|
+
needsRebuild: true,
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Runs xcodebuild with its output in `log`, which it returns. Its failure names the lines that say why, and what to do
|
|
34
|
+
// where those leave it unclear, from crossbind's causes first, then those the caller adds: the error says whether the
|
|
35
|
+
// user can mend it and try again, the page where, and the cause it found.
|
|
36
|
+
export function xcodebuild(what, args, { log, facts = {}, causes = [] }) {
|
|
37
|
+
const out = fs.openSync(log, 'w');
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = spawnSync('xcodebuild', args, { stdio: ['ignore', out, out] });
|
|
41
|
+
} finally {
|
|
42
|
+
fs.closeSync(out);
|
|
43
|
+
}
|
|
44
|
+
if (result.error) throw new Error(`crossbind: cannot run xcodebuild, which comes with Xcode: ${result.error.message}`, { cause: result.error });
|
|
45
|
+
if (result.signal) throw new Error(`crossbind: ${what} stopped on ${result.signal}.`);
|
|
46
|
+
const output = fs.readFileSync(log, 'utf8');
|
|
47
|
+
if (result.status === 0) return output;
|
|
48
|
+
const reasons = [...new Set(output.split('\n').map((line) => line.trim()).filter((line) => /\berror\b|ITMS-/i.test(line)))].slice(-REASON_LINES);
|
|
49
|
+
const cause = [...CAUSES, ...causes].find(({ pattern }) => pattern.test(output));
|
|
50
|
+
const lines = [`crossbind: ${what} failed; xcodebuild's log is ${log}.`, ...reasons.map((line) => ` ${line}`), ...(cause ? [`crossbind: ${cause.hint(facts)}`] : [])];
|
|
51
|
+
throw Object.assign(new Error(lines.join('\n')), { isMendable: Boolean(cause) && !cause.needsRebuild, page: cause?.page ?? null, reason: cause ?? null });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The teams of the accounts signed in to Xcode that upload to App Store Connect, which a free personal team does not,
|
|
55
|
+
// from Xcode's settings; `withPersonal` adds those free teams, which sign for their own devices.
|
|
56
|
+
export function xcodeTeams({ withPersonal = false } = {}) {
|
|
57
|
+
const settings = spawnSync('defaults', ['export', 'com.apple.dt.Xcode', '-'], { maxBuffer: EXEC_MAX_BUFFER });
|
|
58
|
+
if (settings.status !== 0) return [];
|
|
59
|
+
const teams = spawnSync('plutil', ['-extract', 'IDEProvisioningTeamByIdentifier', 'json', '-o', '-', '-'], {
|
|
60
|
+
input: settings.stdout, encoding: 'utf8', maxBuffer: EXEC_MAX_BUFFER,
|
|
61
|
+
});
|
|
62
|
+
if (teams.status !== 0) return [];
|
|
63
|
+
const signing = Object.values(JSON.parse(teams.stdout)).flat().filter((team) => withPersonal || !team.isFreeProvisioningTeam);
|
|
64
|
+
return [...new Map(signing.map((team) => [team.teamID, { id: team.teamID, name: team.teamName }])).values()];
|
|
65
|
+
}
|