1*a272999eSBartosz Kaszubowskiimport fs from 'fs-extra'; 2eeffdb10STomasz Sapetaimport os from 'os'; 3eeffdb10STomasz Sapetaimport path from 'path'; 4eeffdb10STomasz Sapeta 5*a272999eSBartosz Kaszubowskiimport { formatXcodeBuildOutput } from '../Formatter'; 6eeffdb10STomasz Sapetaimport { spawnAsync } from '../Utils'; 7eeffdb10STomasz Sapetaimport { generateXcodeProjectAsync } from './XcodeGen'; 8eeffdb10STomasz Sapetaimport { ProjectSpec } from './XcodeGen.types'; 9*a272999eSBartosz Kaszubowskiimport { Flavor, Framework, XcodebuildSettings } from './XcodeProject.types'; 10eeffdb10STomasz Sapeta 11eeffdb10STomasz Sapeta/** 12eeffdb10STomasz Sapeta * Path to the shared derived data directory. 13eeffdb10STomasz Sapeta */ 14eeffdb10STomasz Sapetaconst SHARED_DERIVED_DATA_DIR = path.join(os.tmpdir(), 'Expo/DerivedData'); 15eeffdb10STomasz Sapeta 16eeffdb10STomasz Sapeta/** 17eeffdb10STomasz Sapeta * Path to the products in derived data directory. We pick `.framework` files from there. 18eeffdb10STomasz Sapeta */ 19eeffdb10STomasz Sapetaconst PRODUCTS_DIR = path.join(SHARED_DERIVED_DATA_DIR, 'Build/Products'); 20eeffdb10STomasz Sapeta 21eeffdb10STomasz Sapeta/** 22eeffdb10STomasz Sapeta * A class representing single Xcode project and operating on its `.xcodeproj` file. 23eeffdb10STomasz Sapeta */ 24eeffdb10STomasz Sapetaexport default class XcodeProject { 25eeffdb10STomasz Sapeta /** 26eeffdb10STomasz Sapeta * Creates `XcodeProject` instance from given path to `.xcodeproj` file. 27eeffdb10STomasz Sapeta */ 28eeffdb10STomasz Sapeta static async fromXcodeprojPathAsync(xcodeprojPath: string): Promise<XcodeProject> { 29eeffdb10STomasz Sapeta if (!(await fs.pathExists(xcodeprojPath))) { 30eeffdb10STomasz Sapeta throw new Error(`Xcodeproj not found at path: ${xcodeprojPath}`); 31eeffdb10STomasz Sapeta } 32eeffdb10STomasz Sapeta return new XcodeProject(xcodeprojPath); 33eeffdb10STomasz Sapeta } 34eeffdb10STomasz Sapeta 35eeffdb10STomasz Sapeta /** 36eeffdb10STomasz Sapeta * Generates `.xcodeproj` file based on given spec and returns it. 37eeffdb10STomasz Sapeta */ 38eeffdb10STomasz Sapeta static async generateProjectFromSpec(dir: string, spec: ProjectSpec): Promise<XcodeProject> { 39eeffdb10STomasz Sapeta const xcodeprojPath = await generateXcodeProjectAsync(dir, spec); 40eeffdb10STomasz Sapeta return new XcodeProject(xcodeprojPath); 41eeffdb10STomasz Sapeta } 42eeffdb10STomasz Sapeta 43eeffdb10STomasz Sapeta /** 44eeffdb10STomasz Sapeta * Name of the project. It should stay in sync with its filename. 45eeffdb10STomasz Sapeta */ 46eeffdb10STomasz Sapeta name: string; 47eeffdb10STomasz Sapeta 48eeffdb10STomasz Sapeta /** 49eeffdb10STomasz Sapeta * Root directory of the project and at which the `.xcodeproj` file is placed. 50eeffdb10STomasz Sapeta */ 51eeffdb10STomasz Sapeta rootDir: string; 52eeffdb10STomasz Sapeta 53eeffdb10STomasz Sapeta constructor(xcodeprojPath: string) { 54eeffdb10STomasz Sapeta this.name = path.basename(xcodeprojPath, '.xcodeproj'); 55eeffdb10STomasz Sapeta this.rootDir = path.dirname(xcodeprojPath); 56eeffdb10STomasz Sapeta } 57eeffdb10STomasz Sapeta 58eeffdb10STomasz Sapeta /** 59eeffdb10STomasz Sapeta * Returns output path to where the `.xcframework` file will be stored after running `buildXcframeworkAsync`. 60eeffdb10STomasz Sapeta */ 61eeffdb10STomasz Sapeta getXcframeworkPath(): string { 62eeffdb10STomasz Sapeta return path.join(this.rootDir, `${this.name}.xcframework`); 63eeffdb10STomasz Sapeta } 64eeffdb10STomasz Sapeta 65eeffdb10STomasz Sapeta /** 66eeffdb10STomasz Sapeta * Builds `.framework` for given target name and flavor specifying, 67eeffdb10STomasz Sapeta * configuration, the SDK and a list of architectures to compile against. 68eeffdb10STomasz Sapeta */ 69eeffdb10STomasz Sapeta async buildFrameworkAsync( 70eeffdb10STomasz Sapeta target: string, 71eeffdb10STomasz Sapeta flavor: Flavor, 72eeffdb10STomasz Sapeta options?: XcodebuildSettings 73eeffdb10STomasz Sapeta ): Promise<Framework> { 74eeffdb10STomasz Sapeta await this.xcodebuildAsync( 75eeffdb10STomasz Sapeta [ 76eeffdb10STomasz Sapeta 'build', 77eeffdb10STomasz Sapeta '-project', 78eeffdb10STomasz Sapeta `${this.name}.xcodeproj`, 79eeffdb10STomasz Sapeta '-scheme', 80eeffdb10STomasz Sapeta `${target}_iOS`, 81eeffdb10STomasz Sapeta '-configuration', 82eeffdb10STomasz Sapeta flavor.configuration, 83eeffdb10STomasz Sapeta '-sdk', 84eeffdb10STomasz Sapeta flavor.sdk, 85eeffdb10STomasz Sapeta ...spreadArgs('-arch', flavor.archs), 86eeffdb10STomasz Sapeta '-derivedDataPath', 87eeffdb10STomasz Sapeta SHARED_DERIVED_DATA_DIR, 88eeffdb10STomasz Sapeta ], 89eeffdb10STomasz Sapeta options 90eeffdb10STomasz Sapeta ); 91eeffdb10STomasz Sapeta 92eeffdb10STomasz Sapeta const frameworkPath = flavorToFrameworkPath(target, flavor); 93eeffdb10STomasz Sapeta const stat = await fs.lstat(path.join(frameworkPath, target)); 94eeffdb10STomasz Sapeta 95eeffdb10STomasz Sapeta // Remove `Headers` as each our module contains headers as part of the provided source code 96eeffdb10STomasz Sapeta // and CocoaPods exposes them through HEADER_SEARCH_PATHS either way. 97eeffdb10STomasz Sapeta await fs.remove(path.join(frameworkPath, 'Headers')); 98eeffdb10STomasz Sapeta 99eeffdb10STomasz Sapeta // `_CodeSignature` is apparently generated only for simulator, afaik we don't need it. 100eeffdb10STomasz Sapeta await fs.remove(path.join(frameworkPath, '_CodeSignature')); 101eeffdb10STomasz Sapeta 102eeffdb10STomasz Sapeta return { 103eeffdb10STomasz Sapeta target, 104eeffdb10STomasz Sapeta flavor, 105eeffdb10STomasz Sapeta frameworkPath, 106eeffdb10STomasz Sapeta binarySize: stat.size, 107eeffdb10STomasz Sapeta }; 108eeffdb10STomasz Sapeta } 109eeffdb10STomasz Sapeta 110eeffdb10STomasz Sapeta /** 111eeffdb10STomasz Sapeta * Builds universal `.xcframework` from given frameworks. 112eeffdb10STomasz Sapeta */ 113eeffdb10STomasz Sapeta async buildXcframeworkAsync( 114eeffdb10STomasz Sapeta frameworks: Framework[], 115eeffdb10STomasz Sapeta options?: XcodebuildSettings 116eeffdb10STomasz Sapeta ): Promise<string> { 117eeffdb10STomasz Sapeta const frameworkPaths = frameworks.map((framework) => framework.frameworkPath); 118eeffdb10STomasz Sapeta const outputPath = this.getXcframeworkPath(); 119eeffdb10STomasz Sapeta 120eeffdb10STomasz Sapeta await fs.remove(outputPath); 121eeffdb10STomasz Sapeta 122eeffdb10STomasz Sapeta await this.xcodebuildAsync( 123eeffdb10STomasz Sapeta ['-create-xcframework', ...spreadArgs('-framework', frameworkPaths), '-output', outputPath], 124eeffdb10STomasz Sapeta options 125eeffdb10STomasz Sapeta ); 126eeffdb10STomasz Sapeta return outputPath; 127eeffdb10STomasz Sapeta } 128eeffdb10STomasz Sapeta 129eeffdb10STomasz Sapeta /** 130eeffdb10STomasz Sapeta * Removes `.xcframework` artifact produced by `buildXcframeworkAsync`. 131eeffdb10STomasz Sapeta */ 132eeffdb10STomasz Sapeta async cleanXcframeworkAsync(): Promise<void> { 133eeffdb10STomasz Sapeta await fs.remove(this.getXcframeworkPath()); 134eeffdb10STomasz Sapeta } 135eeffdb10STomasz Sapeta 136eeffdb10STomasz Sapeta /** 137eeffdb10STomasz Sapeta * Generic function spawning `xcodebuild` process. 138eeffdb10STomasz Sapeta */ 139eeffdb10STomasz Sapeta async xcodebuildAsync(args: string[], settings?: XcodebuildSettings) { 140eeffdb10STomasz Sapeta // `xcodebuild` writes error details to stdout but we don't want to pollute our output if nothing wrong happens. 141eeffdb10STomasz Sapeta // Spawn it quietly, pipe stderr to stdout and pass it to the current process stdout only when it fails. 142eeffdb10STomasz Sapeta const finalArgs = ['-quiet', ...args, '2>&1']; 143eeffdb10STomasz Sapeta 144eeffdb10STomasz Sapeta if (settings) { 145eeffdb10STomasz Sapeta finalArgs.unshift( 146eeffdb10STomasz Sapeta ...Object.entries(settings).map(([key, value]) => { 147eeffdb10STomasz Sapeta return `${key}=${parseXcodeSettingsValue(value)}`; 148eeffdb10STomasz Sapeta }) 149eeffdb10STomasz Sapeta ); 150eeffdb10STomasz Sapeta } 151eeffdb10STomasz Sapeta try { 152eeffdb10STomasz Sapeta await spawnAsync('xcodebuild', finalArgs, { 153eeffdb10STomasz Sapeta cwd: this.rootDir, 154eeffdb10STomasz Sapeta shell: true, 155eeffdb10STomasz Sapeta stdio: ['ignore', 'pipe', 'inherit'], 156eeffdb10STomasz Sapeta }); 157eeffdb10STomasz Sapeta } catch (e) { 158eeffdb10STomasz Sapeta // Print formatted Xcode logs (merged from stdout and stderr). 159eeffdb10STomasz Sapeta process.stdout.write(formatXcodeBuildOutput(e.stdout)); 160eeffdb10STomasz Sapeta throw e; 161eeffdb10STomasz Sapeta } 162eeffdb10STomasz Sapeta } 163eeffdb10STomasz Sapeta 164eeffdb10STomasz Sapeta /** 165eeffdb10STomasz Sapeta * Cleans shared derived data directory. 166eeffdb10STomasz Sapeta */ 167eeffdb10STomasz Sapeta static async cleanBuildFolderAsync(): Promise<void> { 168eeffdb10STomasz Sapeta await fs.remove(SHARED_DERIVED_DATA_DIR); 169eeffdb10STomasz Sapeta } 170eeffdb10STomasz Sapeta} 171eeffdb10STomasz Sapeta 172eeffdb10STomasz Sapeta/** 173eeffdb10STomasz Sapeta * Returns a path to the prebuilt framework for given flavor. 174eeffdb10STomasz Sapeta */ 175eeffdb10STomasz Sapetafunction flavorToFrameworkPath(target: string, flavor: Flavor): string { 176eeffdb10STomasz Sapeta return path.join(PRODUCTS_DIR, `${flavor.configuration}-${flavor.sdk}`, `${target}.framework`); 177eeffdb10STomasz Sapeta} 178eeffdb10STomasz Sapeta 179eeffdb10STomasz Sapeta/** 180eeffdb10STomasz Sapeta * Spreads given args under specific flag. 181eeffdb10STomasz Sapeta * Example: `spreadArgs('-arch', ['arm64', 'x86_64'])` returns `['-arch', 'arm64', '-arch', 'x86_64']` 182eeffdb10STomasz Sapeta */ 183eeffdb10STomasz Sapetafunction spreadArgs(argName: string, args: string[]): string[] { 184eeffdb10STomasz Sapeta return ([] as string[]).concat(...args.map((arg) => [argName, arg])); 185eeffdb10STomasz Sapeta} 186eeffdb10STomasz Sapeta 187eeffdb10STomasz Sapeta/** 188eeffdb10STomasz Sapeta * Converts boolean values to its Xcode build settings format. Value of other type just passes through. 189eeffdb10STomasz Sapeta */ 190eeffdb10STomasz Sapetafunction parseXcodeSettingsValue(value: string | boolean): string { 191eeffdb10STomasz Sapeta if (typeof value === 'boolean') { 192eeffdb10STomasz Sapeta return value ? 'YES' : 'NO'; 193eeffdb10STomasz Sapeta } 194eeffdb10STomasz Sapeta return value; 195eeffdb10STomasz Sapeta} 196