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