1import chalk from 'chalk';
2import fs from 'fs-extra';
3import path from 'path';
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 {
36    return null;
37  }
38}
39
40export async function generateAndroidBuildConstantsFromMacrosAsync(macros) {
41  // android falls back to published dev home if local dev home
42  // doesn't exist or had an error.
43  const isLocalManifestEmpty =
44    !macros.BUILD_MACHINE_KERNEL_MANIFEST || macros.BUILD_MACHINE_KERNEL_MANIFEST === '';
45
46  let versionUsed = 'local';
47  if (process.env.USE_DOGFOODING_PUBLISHED_KERNEL_MANIFEST) {
48    macros.BUILD_MACHINE_KERNEL_MANIFEST = macros.DOGFOODING_PUBLISHED_KERNEL_MANIFEST;
49    versionUsed = 'doogfooding';
50  } else if (isLocalManifestEmpty) {
51    macros.BUILD_MACHINE_KERNEL_MANIFEST = macros.DEV_PUBLISHED_KERNEL_MANIFEST;
52    versionUsed = 'published dev';
53  }
54  console.log(`Using ${chalk.yellow(versionUsed)} version of Expo Home.`);
55
56  delete macros['DEV_PUBLISHED_KERNEL_MANIFEST'];
57  delete macros['DOGFOODING_PUBLISHED_KERNEL_MANIFEST'];
58
59  const definitions = Object.entries(macros).map(
60    ([name, value]) =>
61      `  public static final ${formatJavaType(value)} ${name} = ${formatJavaLiteral(value)};`
62  );
63
64  const source = `
65package host.exp.exponent.generated;
66
67public class ExponentBuildConstants {
68${definitions.join('\n')}
69}`;
70
71  return (
72    `
73// Copyright 2016-present 650 Industries. All rights reserved.
74// @generated by \`expotools android-generate-dynamic-macros\`
75
76${source.trim()}
77`.trim() + '\n'
78  );
79}
80
81async function updateBuildConstants(buildConstantsPath, macros) {
82  console.log(
83    'Generating build config %s ...',
84    chalk.cyan(path.relative(EXPO_DIR, buildConstantsPath))
85  );
86
87  const [source, existingSource] = await Promise.all([
88    generateAndroidBuildConstantsFromMacrosAsync(macros),
89    readExistingSourceAsync(path.resolve(buildConstantsPath)),
90  ]);
91
92  if (source !== existingSource) {
93    await fs.ensureDir(path.dirname(buildConstantsPath));
94    await fs.writeFile(buildConstantsPath, source, 'utf8');
95  }
96}
97
98export default class AndroidMacrosGenerator {
99  async generateAsync(options): Promise<void> {
100    const { buildConstantsPath, macros } = options;
101
102    await updateBuildConstants(path.resolve(buildConstantsPath), macros);
103  }
104
105  async cleanupAsync(options): Promise<void> {
106    // Nothing to clean on Android
107  }
108}
109