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