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