xref: /expo/tools/src/prebuilds/Prebuilder.ts (revision 87ffd749)
1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import path from 'path';
5
6import { Podspec } from '../CocoaPods';
7import { IOS_DIR } from '../Constants';
8import logger from '../Logger';
9import { Package } from '../Packages';
10import {
11  createSpecFromPodspecAsync,
12  generateXcodeProjectAsync,
13  INFO_PLIST_FILENAME,
14} from './XcodeGen';
15import XcodeProject from './XcodeProject';
16import { Flavor, Framework, XcodebuildSettings } from './XcodeProject.types';
17
18const PODS_DIR = path.join(IOS_DIR, 'Pods');
19
20// We will be increasing this list slowly. Once all are enabled,
21// find a better way to ignore some packages that shouldn't be prebuilt (like interfaces).
22export const PACKAGES_TO_PREBUILD = [
23  // 'expo-app-auth',
24  // 'expo-apple-authentication',
25  // 'expo-application',
26  // 'expo-av',
27  // 'expo-background-fetch',
28  'expo-barcode-scanner',
29  // 'expo-battery',
30  // 'expo-blur',
31  // 'expo-brightness',
32  // 'expo-calendar',
33  // 'expo-camera',
34  // 'expo-cellular',
35  // 'expo-constants',
36  'expo-contacts',
37  // 'expo-crypto',
38  // 'expo-device',
39  // 'expo-document-picker',
40  // 'expo-face-detector',
41  'expo-file-system',
42  // 'expo-firebase-analytics',
43  // 'expo-firebase-core',
44  // 'expo-font',
45  'expo-gl',
46  // 'expo-haptics',
47  // 'expo-image-loader',
48  // 'expo-image-manipulator',
49  // 'expo-image-picker',
50  // 'expo-keep-awake',
51  // 'expo-linear-gradient',
52  // 'expo-local-authentication',
53  // 'expo-localization',
54  'expo-location',
55  // 'expo-mail-composer',
56  'expo-media-library',
57  // 'expo-network',
58  'expo-notifications',
59  // 'expo-permissions',
60  'expo-print',
61  // 'expo-screen-capture',
62  // 'expo-screen-orientation',
63  // 'expo-secure-store',
64  'expo-sensors',
65  // 'expo-sharing',
66  // 'expo-sms',
67  // 'expo-speech',
68  'expo-splash-screen',
69  // 'expo-sqlite',
70  // 'expo-store-review',
71  // 'expo-structured-headers',
72  // 'expo-task-manager',
73  // 'expo-updates',
74  // 'expo-video-thumbnails',
75  // 'expo-web-browser',
76  // 'unimodules-app-loader',
77];
78
79export function canPrebuildPackage(pkg: Package): boolean {
80  return PACKAGES_TO_PREBUILD.includes(pkg.packageName);
81}
82
83/**
84 * Automatically generates `.xcodeproj` from podspec and build frameworks.
85 */
86export async function prebuildPackageAsync(
87  pkg: Package,
88  settings?: XcodebuildSettings
89): Promise<void> {
90  if (canPrebuildPackage(pkg)) {
91    const xcodeProject = await generateXcodeProjectSpecAsync(pkg);
92    await buildFrameworksForProjectAsync(xcodeProject, settings);
93    await cleanTemporaryFilesAsync(xcodeProject);
94  }
95}
96
97export async function buildFrameworksForProjectAsync(
98  xcodeProject: XcodeProject,
99  settings?: XcodebuildSettings
100) {
101  const flavors: Flavor[] = [
102    {
103      configuration: 'Release',
104      sdk: 'iphoneos',
105      archs: ['arm64'],
106    },
107    {
108      configuration: 'Release',
109      sdk: 'iphonesimulator',
110      archs: ['x86_64', 'arm64'],
111    },
112  ];
113
114  // Builds frameworks from flavors.
115  const frameworks: Framework[] = [];
116  for (const flavor of flavors) {
117    logger.log('   Building framework for %s', chalk.yellow(flavor.sdk));
118
119    frameworks.push(
120      await xcodeProject.buildFrameworkAsync(xcodeProject.name, flavor, {
121        ONLY_ACTIVE_ARCH: false,
122        BITCODE_GENERATION_MODE: 'bitcode',
123        BUILD_LIBRARY_FOR_DISTRIBUTION: true,
124        DEAD_CODE_STRIPPING: true,
125        DEPLOYMENT_POSTPROCESSING: true,
126        STRIP_INSTALLED_PRODUCT: true,
127        STRIP_STYLE: 'non-global',
128        COPY_PHASE_STRIP: true,
129        GCC_GENERATE_DEBUGGING_SYMBOLS: false,
130        ...settings,
131      })
132    );
133  }
134
135  // Print binary sizes
136  const binarySizes = frameworks.map((framework) =>
137    chalk.magenta((framework.binarySize / 1024 / 1024).toFixed(2) + 'MB')
138  );
139  logger.log('   Binary sizes:', binarySizes.join(', '));
140
141  logger.log('   Merging frameworks to', chalk.magenta(`${xcodeProject.name}.xcframework`));
142
143  // Merge frameworks into universal xcframework
144  await xcodeProject.buildXcframeworkAsync(frameworks, settings);
145}
146
147/**
148 * Removes all temporary files that we generated in order to create `.xcframework` file.
149 */
150export async function cleanTemporaryFilesAsync(xcodeProject: XcodeProject) {
151  logger.log('   Cleaning up temporary files');
152
153  const pathsToRemove = [`${xcodeProject.name}.xcodeproj`, INFO_PLIST_FILENAME];
154
155  await Promise.all(
156    pathsToRemove.map((pathToRemove) => fs.remove(path.join(xcodeProject.rootDir, pathToRemove)))
157  );
158}
159
160/**
161 * Generates Xcode project based on the podspec of given package.
162 */
163export async function generateXcodeProjectSpecAsync(pkg: Package): Promise<XcodeProject> {
164  const podspec = await pkg.getPodspecAsync();
165
166  if (!podspec) {
167    throw new Error('Given package is not an iOS project.');
168  }
169
170  logger.log('   Generating Xcode project spec');
171
172  return await generateXcodeProjectSpecFromPodspecAsync(
173    podspec,
174    path.join(pkg.path, pkg.iosSubdirectory)
175  );
176}
177
178/**
179 * Generates Xcode project based on the given podspec.
180 */
181export async function generateXcodeProjectSpecFromPodspecAsync(
182  podspec: Podspec,
183  dir: string
184): Promise<XcodeProject> {
185  const spec = await createSpecFromPodspecAsync(podspec, async (dependencyName) => {
186    const frameworkPath = await findFrameworkForProjectAsync(dependencyName);
187
188    if (frameworkPath) {
189      return {
190        framework: frameworkPath,
191        link: false,
192        embed: false,
193      };
194    }
195    return null;
196  });
197
198  const xcodeprojPath = await generateXcodeProjectAsync(dir, spec);
199  return await XcodeProject.fromXcodeprojPathAsync(xcodeprojPath);
200}
201
202/**
203 * Removes prebuilt `.xcframework` files for given packages.
204 */
205export async function cleanFrameworksAsync(packages: Package[]) {
206  for (const pkg of packages) {
207    const xcFrameworkFilename = `${pkg.podspecName}.xcframework`;
208    const xcFrameworkPath = path.join(pkg.path, pkg.iosSubdirectory, xcFrameworkFilename);
209
210    if (await fs.pathExists(xcFrameworkPath)) {
211      await fs.remove(xcFrameworkPath);
212    }
213  }
214}
215
216/**
217 * Checks whether given project name has a framework (GoogleSignIn, FBAudience) and returns its path.
218 */
219async function findFrameworkForProjectAsync(projectName: string): Promise<string | null> {
220  const searchNames = new Set([
221    projectName,
222    projectName.replace(/\/+/, ''), // Firebase/MLVision -> FirebaseMLVision
223    projectName.replace(/\/+.*$/, ''), // FacebookSDK/* -> FacebookSDK
224  ]);
225
226  for (const name of searchNames) {
227    const cwd = path.join(PODS_DIR, name);
228
229    if (await fs.pathExists(cwd)) {
230      const paths = await glob(`**/*.framework`, {
231        cwd,
232      });
233
234      if (paths.length > 0) {
235        return path.join(cwd, paths[0]);
236      }
237    }
238  }
239  return null;
240}
241