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