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