xref: /expo/tools/src/prebuilds/XcodeProject.ts (revision a272999e)
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