1import { IosPlist, IosPodsTools } from '@expo/xdl';
2import chalk from 'chalk';
3import fs from 'fs-extra';
4import path from 'path';
5import plist from 'plist';
6
7import * as Directories from '../Directories';
8import * as ProjectVersions from '../ProjectVersions';
9
10interface PlistObject {
11  [key: string]: any;
12}
13
14const EXPO_DIR = Directories.getExpoRepositoryRootDir();
15
16async function readPlistAsync(plistPath: string): Promise<PlistObject> {
17  const plistFileContent = await fs.readFile(plistPath, 'utf8');
18  return plist.parse(plistFileContent);
19}
20
21async function generateBuildConstantsFromMacrosAsync(
22  buildConfigPlistPath,
23  macros,
24  buildConfiguration,
25  infoPlistContents,
26  keys
27): Promise<PlistObject> {
28  const plistPath = path.dirname(buildConfigPlistPath);
29  const plistName = path.basename(buildConfigPlistPath);
30
31  if (!(await fs.pathExists(buildConfigPlistPath))) {
32    await IosPlist.createBlankAsync(plistPath, plistName);
33  }
34
35  console.log(
36    'Generating build config %s ...',
37    chalk.cyan(path.relative(EXPO_DIR, buildConfigPlistPath))
38  );
39
40  const result = await IosPlist.modifyAsync(plistPath, plistName, (config) => {
41    if (config.USE_GENERATED_DEFAULTS === false) {
42      // this flag means don't generate anything, let the user override.
43      return config;
44    }
45
46    for (const [name, value] of Object.entries(macros)) {
47      config[name] = value || '';
48    }
49
50    config.EXPO_RUNTIME_VERSION = infoPlistContents.CFBundleVersion
51      ? infoPlistContents.CFBundleVersion
52      : infoPlistContents.CFBundleShortVersionString;
53
54    if (!config.API_SERVER_ENDPOINT) {
55      config.API_SERVER_ENDPOINT = 'https://exp.host/--/api/v2/';
56    }
57    if (keys) {
58      const { GOOGLE_MAPS_IOS_API_KEY } = keys;
59      config.DEFAULT_API_KEYS = { GOOGLE_MAPS_IOS_API_KEY };
60    }
61    return validateBuildConstants(config, buildConfiguration);
62  });
63
64  return result;
65}
66
67/**
68 *  Adds IS_DEV_KERNEL (bool) and DEV_KERNEL_SOURCE (PUBLISHED, LOCAL)
69 *  and errors if there's a problem with the chosen environment.
70 */
71function validateBuildConstants(config, buildConfiguration) {
72  config.USE_GENERATED_DEFAULTS = true;
73
74  let IS_DEV_KERNEL = false;
75  let DEV_KERNEL_SOURCE = '';
76  if (buildConfiguration === 'Debug') {
77    IS_DEV_KERNEL = true;
78    DEV_KERNEL_SOURCE = config.DEV_KERNEL_SOURCE;
79    if (!DEV_KERNEL_SOURCE) {
80      // default to dev published build if nothing specified
81      DEV_KERNEL_SOURCE = 'PUBLISHED';
82    }
83  } else {
84    IS_DEV_KERNEL = false;
85  }
86
87  if (IS_DEV_KERNEL) {
88    if (DEV_KERNEL_SOURCE === 'LOCAL' && !config.BUILD_MACHINE_KERNEL_MANIFEST) {
89      throw new Error(
90        `Error generating local kernel manifest.\nMake sure a local kernel is being served, or switch DEV_KERNEL_SOURCE to use PUBLISHED instead.`
91      );
92    }
93
94    if (DEV_KERNEL_SOURCE === 'PUBLISHED' && !config.DEV_PUBLISHED_KERNEL_MANIFEST) {
95      throw new Error(`Error downloading DEV published kernel manifest.\n`);
96    }
97
98    if (process.env.USE_DOGFOODING_PUBLISHED_KERNEL_MANIFEST) {
99      if (!config.DOGFOODING_PUBLISHED_KERNEL_MANIFEST) {
100        throw new Error(`Error downloading DOGFOODING published kernel manifest.\n`);
101      }
102      DEV_KERNEL_SOURCE = 'DOGFOODING';
103    }
104  }
105
106  config.IS_DEV_KERNEL = IS_DEV_KERNEL;
107  config.DEV_KERNEL_SOURCE = DEV_KERNEL_SOURCE;
108  return config;
109}
110
111async function writeTemplatesAsync(expoKitPath: string, templateFilesPath: string) {
112  if (expoKitPath) {
113    await renderExpoKitPodspecAsync(expoKitPath, templateFilesPath);
114    await renderExpoKitPodfileAsync(expoKitPath, templateFilesPath);
115  }
116}
117
118export async function renderExpoKitPodspecAsync(
119  expoKitPath: string,
120  templateFilesPath: string
121): Promise<void> {
122  const podspecPath = path.join(expoKitPath, 'ios', 'ExpoKit.podspec');
123  const podspecTemplatePath = path.join(templateFilesPath, 'ios', 'ExpoKit.podspec');
124
125  console.log(
126    'Rendering %s from template %s ...',
127    chalk.cyan(path.relative(EXPO_DIR, podspecPath)),
128    chalk.cyan(path.relative(EXPO_DIR, podspecTemplatePath))
129  );
130
131  await IosPodsTools.renderExpoKitPodspecAsync(podspecTemplatePath, podspecPath, {
132    IOS_EXPONENT_CLIENT_VERSION: await ProjectVersions.getNewestSDKVersionAsync('ios'),
133  });
134}
135
136async function renderExpoKitPodfileAsync(
137  expoKitPath: string,
138  templateFilesPath: string
139): Promise<void> {
140  const podfilePath = path.join(expoKitPath, 'exponent-view-template', 'ios', 'Podfile');
141  const podfileTemplatePath = path.join(templateFilesPath, 'ios', 'ExpoKit-Podfile');
142
143  console.log(
144    'Rendering %s from template %s ...',
145    chalk.cyan(path.relative(EXPO_DIR, podfilePath)),
146    chalk.cyan(path.relative(EXPO_DIR, podfileTemplatePath))
147  );
148
149  await IosPodsTools.renderPodfileAsync(podfileTemplatePath, podfilePath, {
150    TARGET_NAME: 'exponent-view-template',
151    EXPOKIT_PATH: '../..',
152    REACT_NATIVE_PATH: '../../react-native-lab/react-native',
153    UNIVERSAL_MODULES_PATH: '../../packages',
154  });
155}
156
157export default class IosMacrosGenerator {
158  async generateAsync(options): Promise<void> {
159    const { infoPlistPath, buildConstantsPath, macros, templateSubstitutions } = options;
160
161    // Read Info.plist
162    const infoPlist = await readPlistAsync(infoPlistPath);
163
164    // Generate EXBuildConstants.plist
165    await generateBuildConstantsFromMacrosAsync(
166      path.resolve(buildConstantsPath),
167      macros,
168      options.configuration,
169      infoPlist,
170      templateSubstitutions
171    );
172
173    // // Generate Podfile and ExpoKit podspec using template files.
174    await writeTemplatesAsync(options.expoKitPath, options.templateFilesPath);
175  }
176}
177