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