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