xref: /expo/tools/src/prebuilds/Prebuilder.ts (revision f2fbea2e)
1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import path from 'path';
5
6import { IOS_DIR } from '../Constants';
7import logger from '../Logger';
8import { Package } from '../Packages';
9import {
10  createSpecFromPodspecAsync,
11  generateXcodeProjectAsync,
12  INFO_PLIST_FILENAME,
13} from './XcodeGen';
14import XcodeProject from './XcodeProject';
15import { Flavor, Framework, XcodebuildSettings } from './XcodeProject.types';
16
17const PODS_DIR = path.join(IOS_DIR, 'Pods');
18
19// We will be increasing this list slowly. Once all are enabled,
20// find a better way to ignore some packages that shouldn't be prebuilt (like interfaces).
21export const PACKAGES_TO_PREBUILD = [
22  // 'expo-app-auth',
23  // 'expo-apple-authentication',
24  // 'expo-application',
25  'expo-av',
26  // 'expo-background-fetch',
27  'expo-barcode-scanner',
28  // 'expo-battery',
29  // 'expo-blur',
30  // 'expo-brightness',
31  // 'expo-calendar',
32  'expo-camera',
33  // 'expo-cellular',
34  // 'expo-constants',
35  'expo-contacts',
36  // 'expo-crypto',
37  // 'expo-device',
38  // 'expo-document-picker',
39  // 'expo-error-recovery',
40  // 'expo-face-detector',
41  'expo-file-system',
42  // 'expo-firebase-analytics',
43  // 'expo-firebase-core',
44  // 'expo-font',
45  'expo-gl-cpp',
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  const spec = await createSpecFromPodspecAsync(podspec, async (dependencyName) => {
174    const frameworkPath = await findFrameworkForProjectAsync(dependencyName);
175
176    if (frameworkPath) {
177      return {
178        framework: frameworkPath,
179        link: false,
180        embed: false,
181      };
182    }
183    return null;
184  });
185
186  const xcodeprojPath = await generateXcodeProjectAsync(
187    path.join(pkg.path, pkg.iosSubdirectory),
188    spec
189  );
190  return await XcodeProject.fromXcodeprojPathAsync(xcodeprojPath);
191}
192
193/**
194 * Removes prebuilt `.xcframework` files for given packages.
195 */
196export async function cleanFrameworksAsync(packages: Package[]) {
197  for (const pkg of packages) {
198    const xcFrameworkFilename = `${pkg.podspecName}.xcframework`;
199    const xcFrameworkPath = path.join(pkg.path, pkg.iosSubdirectory, xcFrameworkFilename);
200
201    if (await fs.pathExists(xcFrameworkPath)) {
202      await fs.remove(xcFrameworkPath);
203    }
204  }
205}
206
207/**
208 * Checks whether given project name has a framework (GoogleSignIn, FBAudience) and returns its path.
209 */
210async function findFrameworkForProjectAsync(projectName: string): Promise<string | null> {
211  const searchNames = new Set([
212    projectName,
213    projectName.replace(/\/+/, ''), // Firebase/MLVision -> FirebaseMLVision
214    projectName.replace(/\/+.*$/, ''), // FacebookSDK/* -> FacebookSDK
215  ]);
216
217  for (const name of searchNames) {
218    const cwd = path.join(PODS_DIR, name);
219
220    if (await fs.pathExists(cwd)) {
221      const paths = await glob(`**/*.framework`, {
222        cwd,
223      });
224
225      if (paths.length > 0) {
226        return path.join(cwd, paths[0]);
227      }
228    }
229  }
230  return null;
231}
232