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
99  config.IS_DEV_KERNEL = IS_DEV_KERNEL;
100  config.DEV_KERNEL_SOURCE = DEV_KERNEL_SOURCE;
101  return config;
102}
103
104async function writeTemplatesAsync(expoKitPath: string, templateFilesPath: string) {
105  if (expoKitPath) {
106    await renderExpoKitPodspecAsync(expoKitPath, templateFilesPath);
107    await renderExpoKitPodfileAsync(expoKitPath, templateFilesPath);
108  }
109}
110
111export async function renderExpoKitPodspecAsync(
112  expoKitPath: string,
113  templateFilesPath: string
114): Promise<void> {
115  const podspecPath = path.join(expoKitPath, 'ios', 'ExpoKit.podspec');
116  const podspecTemplatePath = path.join(templateFilesPath, 'ios', 'ExpoKit.podspec');
117
118  console.log(
119    'Rendering %s from template %s ...',
120    chalk.cyan(path.relative(EXPO_DIR, podspecPath)),
121    chalk.cyan(path.relative(EXPO_DIR, podspecTemplatePath))
122  );
123
124  await IosPodsTools.renderExpoKitPodspecAsync(podspecTemplatePath, podspecPath, {
125    IOS_EXPONENT_CLIENT_VERSION: await ProjectVersions.getNewestSDKVersionAsync('ios'),
126  });
127}
128
129async function renderExpoKitPodfileAsync(
130  expoKitPath: string,
131  templateFilesPath: string
132): Promise<void> {
133  const podfilePath = path.join(expoKitPath, 'exponent-view-template', 'ios', 'Podfile');
134  const podfileTemplatePath = path.join(templateFilesPath, 'ios', 'ExpoKit-Podfile');
135
136  console.log(
137    'Rendering %s from template %s ...',
138    chalk.cyan(path.relative(EXPO_DIR, podfilePath)),
139    chalk.cyan(path.relative(EXPO_DIR, podfileTemplatePath))
140  );
141
142  await IosPodsTools.renderPodfileAsync(podfileTemplatePath, podfilePath, {
143    TARGET_NAME: 'exponent-view-template',
144    EXPOKIT_PATH: '../..',
145    REACT_NATIVE_PATH: '../../react-native-lab/react-native',
146    UNIVERSAL_MODULES_PATH: '../../packages',
147  });
148}
149
150export default class IosMacrosGenerator {
151  async generateAsync(options): Promise<void> {
152    const { infoPlistPath, buildConstantsPath, macros, templateSubstitutions } = options;
153
154    // Read Info.plist
155    const infoPlist = await readPlistAsync(infoPlistPath);
156
157    // Generate EXBuildConstants.plist
158    await generateBuildConstantsFromMacrosAsync(
159      path.resolve(buildConstantsPath),
160      macros,
161      options.configuration,
162      infoPlist,
163      templateSubstitutions
164    );
165
166    // // Generate Podfile and ExpoKit podspec using template files.
167    await writeTemplatesAsync(options.expoKitPath, options.templateFilesPath);
168  }
169}
170