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