1import path from 'path';
2import fs from 'fs-extra';
3import chalk from 'chalk';
4
5import * as Directories from '../Directories';
6
7const EXPO_DIR = Directories.getExpoRepositoryRootDir();
8
9function formatJavaType(value) {
10  if (value == null) {
11    return 'String';
12  } else if (typeof value === 'string') {
13    return 'String';
14  } else if (typeof value === 'number') {
15    return 'int';
16  }
17  throw new Error(`Unsupported literal value: ${value}`);
18}
19
20function formatJavaLiteral(value) {
21  if (value == null) {
22    return 'null';
23  } else if (typeof value === 'string') {
24    value = value.replace(/"/g, '\\"');
25    return `"${value}"`;
26  } else if (typeof value === 'number') {
27    return value;
28  }
29  throw new Error(`Unsupported literal value: ${value}`);
30}
31
32async function readExistingSourceAsync(filepath) {
33  try {
34    return await fs.readFile(filepath, 'utf8');
35  } catch (e) {
36    return null;
37  }
38}
39
40export async function generateAndroidBuildConstantsFromMacrosAsync(macros) {
41  let source;
42
43  // android falls back to published dev home if local dev home
44  // doesn't exist or had an error.
45  const isLocalManifestEmpty =
46    !macros.BUILD_MACHINE_KERNEL_MANIFEST || macros.BUILD_MACHINE_KERNEL_MANIFEST === '';
47
48  if (isLocalManifestEmpty) {
49    macros.BUILD_MACHINE_KERNEL_MANIFEST = macros.DEV_PUBLISHED_KERNEL_MANIFEST;
50  }
51
52  console.log(
53    `Using ${chalk.yellow(isLocalManifestEmpty ? 'published dev' : 'local')} version of Expo Home.`
54  );
55
56  delete macros['DEV_PUBLISHED_KERNEL_MANIFEST'];
57
58  const definitions = Object.entries(macros).map(
59    ([name, value]) =>
60      `  public static final ${formatJavaType(value)} ${name} = ${formatJavaLiteral(value)};`
61  );
62
63  source = `
64package host.exp.exponent.generated;
65
66public class ExponentBuildConstants {
67${definitions.join('\n')}
68}`;
69
70  return (
71    `
72// Copyright 2016-present 650 Industries. All rights reserved.
73// @generated by \`expotools android-generate-dynamic-macros\`
74
75${source.trim()}
76`.trim() + '\n'
77  );
78}
79
80async function updateBuildConstants(buildConstantsPath, macros) {
81  console.log(
82    'Generating build config %s ...',
83    chalk.cyan(path.relative(EXPO_DIR, buildConstantsPath))
84  );
85
86  const [source, existingSource] = await Promise.all([
87    generateAndroidBuildConstantsFromMacrosAsync(macros),
88    readExistingSourceAsync(path.resolve(buildConstantsPath)),
89  ]);
90
91  if (source !== existingSource) {
92    await fs.ensureDir(path.dirname(buildConstantsPath));
93    await fs.writeFile(buildConstantsPath, source, 'utf8');
94  }
95}
96
97export default class AndroidMacrosGenerator {
98  async generateAsync(options): Promise<void> {
99    const { buildConstantsPath, macros } = options;
100
101    await updateBuildConstants(path.resolve(buildConstantsPath), macros);
102  }
103
104  async cleanupAsync(options): Promise<void> {
105    // Nothing to clean on Android
106  }
107}
108