1import fs from 'fs-extra'; 2import path from 'path'; 3import semver from 'semver'; 4 5import { Podspec } from '../CocoaPods'; 6import { EXPOTOOLS_DIR, IOS_DIR } from '../Constants'; 7import { arrayize, spawnAsync } from '../Utils'; 8import { 9 ProjectSpec, 10 ProjectSpecDependency, 11 ProjectSpecPlatform, 12 XcodeConfig, 13} from './XcodeGen.types'; 14 15const PODS_DIR = path.join(IOS_DIR, 'Pods'); 16const PODS_PUBLIC_HEADERS_DIR = path.join(PODS_DIR, 'Headers', 'Public'); 17const PODS_PRIVATE_HEADERS_DIR = path.join(PODS_DIR, 'Headers', 'Private'); 18const PLATFORMS_MAPPING: Record<string, ProjectSpecPlatform> = { 19 ios: 'iOS', 20 osx: 'macOS', 21 macos: 'macOS', 22 tvos: 'tvOS', 23 watchos: 'watchOS', 24}; 25 26export const INFO_PLIST_FILENAME = 'Info-generated.plist'; 27 28/** 29 * Generates `.xcodeproj` from given project spec and saves it at given dir. 30 */ 31export async function generateXcodeProjectAsync(dir: string, spec: ProjectSpec): Promise<string> { 32 const specPath = path.join(dir, `${spec.name}.spec.json`); 33 34 // Save the spec to the file so `xcodegen` can use it. 35 await fs.outputJSON(specPath, spec, { 36 spaces: 2, 37 }); 38 39 // Generate `.xcodeproj` from given spec. The binary is provided by `@expo/xcodegen` package. 40 await spawnAsync('yarn', ['--silent', 'run', 'xcodegen', '--quiet', '--spec', specPath], { 41 cwd: EXPOTOOLS_DIR, 42 stdio: 'inherit', 43 }); 44 45 // Remove temporary spec file. 46 await fs.remove(specPath); 47 48 return path.join(dir, `${spec.name}.xcodeproj`); 49} 50 51/** 52 * Creates `xcodegen` spec from the podspec. It's very naive, but covers all our cases so far. 53 */ 54export async function createSpecFromPodspecAsync( 55 podspec: Podspec, 56 dependencyResolver: (dependencyName: string) => Promise<ProjectSpecDependency | null> 57): Promise<ProjectSpec> { 58 const platforms = Object.keys(podspec.platforms); 59 const deploymentTarget = platforms.reduce((acc, platform) => { 60 acc[PLATFORMS_MAPPING[platform]] = podspec.platforms[platform]; 61 return acc; 62 }, {} as Record<ProjectSpecPlatform, string>); 63 64 const dependenciesNames = podspec.dependencies ? Object.keys(podspec.dependencies) : []; 65 66 const dependencies = ( 67 await Promise.all(dependenciesNames.map((dependencyName) => dependencyResolver(dependencyName))) 68 ).filter(Boolean) as ProjectSpecDependency[]; 69 70 const bundleId = podNameToBundleId(podspec.name); 71 72 return { 73 name: podspec.name, 74 targets: { 75 [podspec.name]: { 76 type: 'framework', 77 platform: platforms.map((platform) => PLATFORMS_MAPPING[platform]), 78 sources: [ 79 { 80 path: '', 81 name: podspec.name, 82 createIntermediateGroups: true, 83 includes: arrayize(podspec.source_files), 84 excludes: [ 85 INFO_PLIST_FILENAME, 86 `${podspec.name}.spec.json`, 87 '*.xcodeproj', 88 '*.xcframework', 89 '*.podspec', 90 ...arrayize(podspec.exclude_files), 91 ], 92 compilerFlags: podspec.compiler_flags, 93 }, 94 ], 95 dependencies: [ 96 ...arrayize(podspec.frameworks).map((framework) => ({ 97 sdk: `${framework}.framework`, 98 })), 99 ...dependencies, 100 ], 101 settings: { 102 base: mergeXcodeConfigs(podspec.pod_target_xcconfig ?? {}, { 103 MACH_O_TYPE: 'staticlib', 104 }), 105 }, 106 info: { 107 path: INFO_PLIST_FILENAME, 108 properties: mergeXcodeConfigs( 109 { 110 CFBundleIdentifier: bundleId, 111 CFBundleName: podspec.name, 112 CFBundleShortVersionString: podspec.version, 113 CFBundleVersion: String(semver.major(podspec.version)), 114 }, 115 podspec.info_plist ?? {} 116 ), 117 }, 118 }, 119 }, 120 options: { 121 minimumXcodeGenVersion: '2.18.0', 122 deploymentTarget, 123 }, 124 settings: { 125 base: { 126 PRODUCT_BUNDLE_IDENTIFIER: bundleId, 127 IPHONEOS_DEPLOYMENT_TARGET: podspec.platforms.ios, 128 FRAMEWORK_SEARCH_PATHS: constructFrameworkSearchPaths(dependencies), 129 HEADER_SEARCH_PATHS: constructHeaderSearchPaths(dependenciesNames), 130 131 // Suppresses deprecation warnings coming from frameworks like OpenGLES. 132 VALIDATE_WORKSPACE_SKIPPED_SDK_FRAMEWORKS: arrayize(podspec.frameworks).join(' '), 133 }, 134 }, 135 }; 136} 137 138function constructFrameworkSearchPaths(dependencies: ProjectSpecDependency[]): string { 139 const frameworks = dependencies.filter((dependency) => !!dependency.framework) as { 140 framework: string; 141 }[]; 142 143 return ( 144 '$(inherited) ' + frameworks.map((dependency) => path.dirname(dependency.framework)).join(' ') 145 ).trim(); 146} 147 148function constructHeaderSearchPaths(dependencies: string[]): string { 149 // A set of pod names to include in header search paths. 150 // For simplicity, we add some more (usually transitive) than direct dependencies. 151 const podsToSearchForHeaders = new Set([ 152 // Some pods' have headers at its root level (ZXingObjC and all our modules). 153 // Without this we would have to use `#import <ZXingObjC*.h>` instead of `#import <ZXingObjC/ZXingObjC*.h>` 154 '', 155 156 ...dependencies, 157 158 'DoubleConversion', 159 'React-callinvoker', 160 'React-Core', 161 'React-cxxreact', 162 'React-jsi', 163 'React-jsiexecutor', 164 'React-jsinspector', 165 'Yoga', 166 'glog', 167 ]); 168 169 function headerSearchPathsForDir(dir: string): string { 170 return [...podsToSearchForHeaders] 171 .map((podName) => '"' + path.join(dir, podName) + '"') 172 .join(' '); 173 } 174 175 return [ 176 '$(inherited)', 177 headerSearchPathsForDir(PODS_PUBLIC_HEADERS_DIR), 178 headerSearchPathsForDir(PODS_PRIVATE_HEADERS_DIR), 179 ].join(' '); 180} 181 182/** 183 * Merges Xcode config from left to right. 184 * Values containing `$(inherited)` are properly taken into account. 185 */ 186function mergeXcodeConfigs(...configs: XcodeConfig[]): XcodeConfig { 187 const result: XcodeConfig = {}; 188 189 for (const config of configs) { 190 for (const key in config) { 191 const value = config[key]; 192 result[key] = mergeXcodeConfigValue(result[key], value); 193 } 194 } 195 return result; 196} 197 198function mergeXcodeConfigValue(prevValue: string | undefined, nextValue: string): string { 199 if (prevValue && typeof prevValue === 'string' && prevValue.includes('$(inherited)')) { 200 return '$(inherited) ' + (prevValue + ' ' + nextValue).replace(/\\s*$\(inherited\)\s*/g, ' '); 201 } 202 return nextValue; 203} 204 205/** 206 * Simple conversion from pod name to framework's bundle identifier. 207 */ 208function podNameToBundleId(podName: string): string { 209 return podName 210 .replace(/^UM/, 'unimodules') 211 .replace(/^EX/, 'expo') 212 .replace(/(\_|[^\w\d\.])+/g, '.') 213 .replace(/\.*([A-Z]+)/g, (_, p1) => `.${p1.toLowerCase()}`); 214} 215