crossbind 2.0.0-beta.68 → 2.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/package.json +69 -59
  3. package/src/actions/addExtension.js +191 -0
  4. package/src/actions/androidGradle.js +26 -17
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/buildApp.js +172 -74
  8. package/src/actions/buildNode.js +6 -1
  9. package/src/actions/buildWasm.js +0 -9
  10. package/src/actions/createInterface.js +17 -8
  11. package/src/actions/createLib.js +0 -4
  12. package/src/actions/extensionApi.js +298 -0
  13. package/src/actions/extensionCommands.js +41 -0
  14. package/src/actions/extensionsInApp.js +19 -0
  15. package/src/actions/gradleProject.js +196 -16
  16. package/src/actions/iosDevice.js +2 -2
  17. package/src/actions/iosSimulator.js +3 -3
  18. package/src/actions/licenses.js +83 -34
  19. package/src/actions/linuxPackages.js +48 -0
  20. package/src/actions/mavenVerification.js +37 -0
  21. package/src/actions/publish.js +220 -0
  22. package/src/actions/windowsSigning.js +1 -1
  23. package/src/actions/xcodeProject.js +49 -24
  24. package/src/actions/xcodeTargets.js +122 -0
  25. package/src/actions/xcodebuild.js +65 -0
  26. package/src/app/index.d.ts +126 -133
  27. package/src/app/index.js +126 -188
  28. package/src/assets/configure/install.sh +0 -0
  29. package/src/assets/toolchain-digests.json +20 -20
  30. package/src/bin.js +20 -28
  31. package/src/index.js +2 -1
  32. package/src/program.js +32 -3
  33. package/src/state/index.js +2 -1
  34. package/src/state/loadConfig.js +16 -5
  35. package/src/utils/appExtensions.js +761 -0
  36. package/src/utils/appFiles.js +58 -0
  37. package/src/utils/appLaunches.js +249 -0
  38. package/src/utils/appOptions.js +109 -100
  39. package/src/utils/appTargets.js +168 -0
  40. package/src/utils/bridgeTools.js +66 -0
  41. package/src/utils/configChanges.js +30 -0
  42. package/src/utils/configWriter.js +143 -26
  43. package/src/utils/extensionSteps.js +135 -0
  44. package/src/utils/openUrl.js +2 -1
  45. package/src/utils/plist.js +5 -0
  46. package/src/utils/resolveAppHost.js +7 -0
  47. package/src/utils/systemConfig.js +16 -3
  48. package/vitest.config.js +6 -1
  49. package/src/actions/appSetup.js +0 -116
  50. package/src/actions/appStoreConnect.js +0 -188
  51. package/src/actions/appUpdate.js +0 -68
  52. package/src/actions/extensions.js +0 -7
  53. package/src/actions/googlePlay.js +0 -213
  54. package/src/actions/googleSignIn.js +0 -78
  55. package/src/actions/playSetup.js +0 -19
  56. package/src/actions/publishAndroid.js +0 -102
  57. package/src/actions/publishIos.js +0 -146
@@ -3,13 +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 { PRIVACY_CATEGORIES } from '../utils/appOptions.js';
6
+ import { mergedPlist } from '../utils/appExtensions.js';
7
+ import { appLinkParts, iosDocumentTypes, iosShortcutItems } from '../utils/appLaunches.js';
8
+ import { privacyManifest, writeTargets } from './xcodeTargets.js';
7
9
 
8
10
  // The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
9
11
  export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
10
12
  export const HERMES_SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64_x86_64-simulator' };
11
13
  // What the host's own code needs from the system: Xcode adds it only for the sources it compiles itself.
12
- 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++'];
13
15
 
14
16
  const MAIN = `// crossbind writes this project anew on every build: the app comes prebuilt in lib/, and its files are in app/.
15
17
  int crossbind_app_main(int argc, char *argv[]);
@@ -19,7 +21,7 @@ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
19
21
 
20
22
  const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
21
23
  'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
22
- 'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild', 'privacy', 'privacyBuild'];
24
+ 'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild', 'privacy', 'privacyBuild', 'embedTargets'];
23
25
  // Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
24
26
  const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
25
27
 
@@ -40,7 +42,14 @@ const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationL
40
42
  // The color set of app.window.backgroundColor, which the launch screen shows.
41
43
  const LAUNCH_COLOR = 'LaunchBackground';
42
44
 
43
- const infoPlist = (name, urlSchemes, hasBackground, usage, encryption) => xmlPlist({
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({
44
53
  CFBundleDevelopmentRegion: 'en',
45
54
  CFBundleDisplayName: name,
46
55
  CFBundleExecutable: '$(EXECUTABLE_NAME)',
@@ -53,15 +62,20 @@ const infoPlist = (name, urlSchemes, hasBackground, usage, encryption) => xmlPli
53
62
  CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
54
63
  ...(encryption === null ? {} : { ITSAppUsesNonExemptEncryption: encryption }),
55
64
  LSRequiresIPhoneOS: true,
56
- UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
65
+ UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: multipleWindows },
57
66
  UILaunchScreen: hasBackground ? { UIColorName: LAUNCH_COLOR } : {},
58
67
  UISupportedInterfaceOrientations: ORIENTATIONS,
59
68
  'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
69
+ ...iosDocumentTypes(fileTypes, '$(PRODUCT_BUNDLE_IDENTIFIER)'),
70
+ ...(activityTypes.length > 0 ? { NSUserActivityTypes: activityTypes } : {}),
71
+ ...(shortcutItems.length > 0 ? { UIApplicationShortcutItems: iosShortcutItems(shortcutItems) } : {}),
60
72
  // The reasons of app.permissions, without which iOS lets the app use none of those devices.
61
73
  ...usage,
62
- });
74
+ ...(appGroup ? { CrossbindAppGroup: appGroup } : {}),
75
+ }, plist));
63
76
 
64
- function objects(name, targetSettings, hasAssets, hasPrivacy) {
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) {
65
79
  const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
66
80
  const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
67
81
  const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
@@ -77,9 +91,11 @@ function objects(name, targetSettings, hasAssets, hasPrivacy) {
77
91
  [ID.privacy]: file('PrivacyInfo.xcprivacy', 'text.xml'),
78
92
  [ID.privacyBuild]: { isa: 'PBXBuildFile', fileRef: ID.privacy },
79
93
  } : {};
94
+ const hasTargets = added.targets.length > 0;
80
95
  return {
81
96
  ...assets,
82
97
  ...privacy,
98
+ ...added.objects,
83
99
  [ID.main]: file('main.m', 'sourcecode.c.objc'),
84
100
  [ID.info]: file('Info.plist', 'text.plist.xml'),
85
101
  [ID.app]: file('app', 'folder'),
@@ -95,18 +111,19 @@ function objects(name, targetSettings, hasAssets, hasPrivacy) {
95
111
  [ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
96
112
  [ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : []), ...(hasPrivacy ? [ID.privacyBuild] : [])]),
97
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' }) } : {}),
98
115
  [ID.mainGroup]: {
99
116
  isa: 'PBXGroup',
100
- children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ...(hasPrivacy ? [ID.privacy] : []), ID.hermes, ID.products],
117
+ children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ...(hasPrivacy ? [ID.privacy] : []), ID.hermes, ...added.groups, ID.products],
101
118
  sourceTree: '<group>',
102
119
  },
103
- [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>' },
104
121
  [ID.target]: {
105
122
  isa: 'PBXNativeTarget',
106
123
  buildConfigurationList: ID.targetConfigs,
107
- buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed],
124
+ buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed, ...(hasTargets ? [ID.embedTargets] : [])],
108
125
  buildRules: [],
109
- dependencies: [],
126
+ dependencies: added.dependencies,
110
127
  name,
111
128
  productName: name,
112
129
  productReference: ID.product,
@@ -123,7 +140,7 @@ function objects(name, targetSettings, hasAssets, hasPrivacy) {
123
140
  productRefGroup: ID.products,
124
141
  projectDirPath: '',
125
142
  projectRoot: '',
126
- targets: [ID.target],
143
+ targets: [ID.target, ...added.targets],
127
144
  },
128
145
  [ID.projectDebug]: configuration('Debug', PROJECT_SETTINGS),
129
146
  [ID.projectRelease]: configuration('Release', PROJECT_SETTINGS),
@@ -152,22 +169,26 @@ function writeAssets(catalog, icon, background) {
152
169
  }
153
170
  }
154
171
 
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
-
163
172
  // An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
164
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
165
- // Simulator or on a device, and makes every size of its icon from the one PNG given.
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.
166
177
  export default function writeXcodeProject(to, {
167
- parts, identity, label, version, build = version, icon = null, urlSchemes = [], background = null, usage = {}, encryption = null, privacy = {},
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,
168
180
  }) {
169
181
  const { name } = state.config.general;
170
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] : []);
171
192
  fs.rmSync(to, { recursive: true, force: true });
172
193
  fs.mkdirSync(`${to}/${name}.xcodeproj`, { recursive: true });
173
194
  const linkFlags = {};
@@ -184,24 +205,28 @@ export default function writeXcodeProject(to, {
184
205
  if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
185
206
 
186
207
  fs.writeFileSync(`${to}/main.m`, MAIN);
187
- fs.writeFileSync(`${to}/Info.plist`, infoPlist(label, urlSchemes, Boolean(background), usage, encryption));
208
+ fs.writeFileSync(`${to}/Info.plist`, info);
188
209
  writeAssets(`${to}/Assets.xcassets`, icon, background);
189
210
  const hasPrivacy = Object.keys(privacy).length > 0;
190
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));
191
214
  const targetSettings = {
192
215
  ...(icon ? { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon' } : {}),
193
216
  PRODUCT_NAME: name,
194
217
  PRODUCT_BUNDLE_IDENTIFIER: identity.id,
195
218
  DEVELOPMENT_TEAM: identity.team,
196
219
  CODE_SIGN_STYLE: 'Automatic',
220
+ ...(hasEntitlements ? { CODE_SIGN_ENTITLEMENTS: `${name}.entitlements` } : {}),
197
221
  INFOPLIST_FILE: 'Info.plist',
198
222
  MARKETING_VERSION: version,
199
223
  CURRENT_PROJECT_VERSION: build,
200
224
  LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
201
225
  ...linkFlags,
202
226
  };
227
+ const added = writeTargets(to, targets, { project: ID.project, identity, label, version, build, appGroup });
203
228
  fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
204
- archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background), hasPrivacy), rootObject: ID.project,
229
+ archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background), hasPrivacy, added), rootObject: ID.project,
205
230
  }));
206
231
  return to;
207
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
+ }