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