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