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 chunkString(s: string, len: number) {
21  const size = Math.ceil(s.length / len);
22  const r = Array(size);
23  let offset = 0;
24
25  for (let i = 0; i < size; i++) {
26    r[i] = s.substr(offset, len);
27    offset += len;
28  }
29
30  return r;
31}
32
33function formatJavaLiteral(value) {
34  if (value == null) {
35    return 'null';
36  } else if (typeof value === 'string') {
37    return `"${value.replace(/"/g, '\\"')}"`;
38  } else if (typeof value === 'number') {
39    return value;
40  }
41  throw new Error(`Unsupported literal value: ${value}`);
42}
43
44async function readExistingSourceAsync(filepath) {
45  try {
46    return await fs.readFile(filepath, 'utf8');
47  } catch {
48    return null;
49  }
50}
51
52export async function generateAndroidBuildConstantsFromMacrosAsync(macros) {
53  // android falls back to published dev home if local dev home
54  // doesn't exist or had an error.
55  const isLocalManifestEmpty =
56    !macros.BUILD_MACHINE_KERNEL_MANIFEST || macros.BUILD_MACHINE_KERNEL_MANIFEST === '';
57
58  let versionUsed = 'local';
59  if (isLocalManifestEmpty) {
60    macros.BUILD_MACHINE_KERNEL_MANIFEST = macros.DEV_PUBLISHED_KERNEL_MANIFEST;
61    versionUsed = 'published dev';
62  }
63  console.log(`Using ${chalk.yellow(versionUsed)} version of Expo Home.`);
64
65  delete macros['DEV_PUBLISHED_KERNEL_MANIFEST'];
66
67  const BUILD_MACHINE_KERNEL_MANIFEST = macros.BUILD_MACHINE_KERNEL_MANIFEST;
68
69  delete macros['BUILD_MACHINE_KERNEL_MANIFEST'];
70
71  const definitions = Object.entries(macros).map(
72    ([name, value]) =>
73      `  public static final ${formatJavaType(value)} ${name} = ${formatJavaLiteral(value)};`
74  );
75
76  const functions = BUILD_MACHINE_KERNEL_MANIFEST
77    ? `
78  public static String getBuildMachineKernelManifestAndAssetRequestHeaders() {
79    return new StringBuilder()${chunkString(BUILD_MACHINE_KERNEL_MANIFEST, 1000)
80      .map((s) => `\n.append("${s.replace(/"/g, '\\"')}")`)
81      .join('')}.toString();
82  }
83  `
84    : null;
85
86  const source = `
87package host.exp.exponent.generated;
88
89public class ExponentBuildConstants {
90${definitions.join('\n')}
91
92${functions}
93}`;
94
95  return (
96    `
97// Copyright 2016-present 650 Industries. All rights reserved.
98// @generated by \`expotools android-generate-dynamic-macros\`
99
100${source.trim()}
101`.trim() + '\n'
102  );
103}
104
105async function updateBuildConstants(buildConstantsPath, macros) {
106  console.log(
107    'Generating build config %s ...',
108    chalk.cyan(path.relative(EXPO_DIR, buildConstantsPath))
109  );
110
111  const [source, existingSource] = await Promise.all([
112    generateAndroidBuildConstantsFromMacrosAsync(macros),
113    readExistingSourceAsync(path.resolve(buildConstantsPath)),
114  ]);
115
116  if (source !== existingSource) {
117    await fs.ensureDir(path.dirname(buildConstantsPath));
118    await fs.writeFile(buildConstantsPath, source, 'utf8');
119  }
120}
121
122export default class AndroidMacrosGenerator {
123  async generateAsync(options): Promise<void> {
124    const { buildConstantsPath, macros } = options;
125
126    await updateBuildConstants(path.resolve(buildConstantsPath), macros);
127  }
128
129  async cleanupAsync(options): Promise<void> {
130    // Nothing to clean on Android
131  }
132}
133