1import spawnAsync, { SpawnResult } from '@expo/spawn-async';
2import path from 'path';
3
4import { Log } from '../../../log';
5import { env } from '../../../utils/env';
6import { AbortCommandError } from '../../../utils/errors';
7
8function upperFirst(name: string) {
9  return name.charAt(0).toUpperCase() + name.slice(1);
10}
11
12/** Format gradle assemble arguments. Exposed for testing.  */
13export function formatGradleArguments(
14  cmd: 'assemble' | 'install',
15  {
16    appName,
17    variant,
18    tasks = [cmd + upperFirst(variant)],
19  }: { tasks?: string[]; variant: string; appName: string }
20): string[] {
21  return appName ? tasks.map((task) => `${appName}:${task}`) : tasks;
22}
23
24function resolveGradleWPath(androidProjectPath: string): string {
25  return path.join(androidProjectPath, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew');
26}
27
28function getPortArg(port: number): string {
29  return `-PreactNativeDevServerPort=${port}`;
30}
31
32/**
33 * Build the Android project using Gradle.
34 *
35 * @param androidProjectPath - Path to the Android project like `projectRoot/android`.
36 * @param props.variant - Variant to install.
37 * @param props.appName - Name of the 'app' folder, this appears to always be `app`.
38 * @param props.port - Dev server port to pass to the install command.
39 * @param props.buildCache - Should use the `--build-cache` flag, enabling the [Gradle build cache](https://docs.gradle.org/current/userguide/build_cache.html).
40 * @returns - A promise resolving to spawn results.
41 */
42export async function assembleAsync(
43  androidProjectPath: string,
44  {
45    variant,
46    port,
47    appName,
48    buildCache,
49  }: {
50    variant: string;
51    port?: number;
52    appName: string;
53    buildCache?: boolean;
54  }
55): Promise<SpawnResult> {
56  const task = formatGradleArguments('assemble', { variant, appName });
57  const args = [
58    ...task,
59    // ignore linting errors
60    '-x',
61    'lint',
62    // ignore tests
63    '-x',
64    'test',
65    '--configure-on-demand',
66  ];
67
68  if (buildCache) args.push('--build-cache');
69
70  // Generate a profile under `/android/app/build/reports/profile`
71  if (env.EXPO_PROFILE) args.push('--profile');
72
73  return await spawnGradleAsync(androidProjectPath, { port, args });
74}
75
76/**
77 * Install an app on device or emulator using `gradlew install`.
78 *
79 * @param androidProjectPath - Path to the Android project like `projectRoot/android`.
80 * @param props.variant - Variant to install.
81 * @param props.appName - Name of the 'app' folder, this appears to always be `app`.
82 * @param props.port - Dev server port to pass to the install command.
83 * @returns - A promise resolving to spawn results.
84 */
85export async function installAsync(
86  androidProjectPath: string,
87  {
88    variant,
89    appName,
90    port,
91  }: {
92    variant: string;
93    appName: string;
94    port?: number;
95  }
96): Promise<SpawnResult> {
97  const args = formatGradleArguments('install', { variant, appName });
98  return await spawnGradleAsync(androidProjectPath, { port, args });
99}
100
101export async function spawnGradleAsync(
102  projectRoot: string,
103  { port, args }: { port?: number; args: string[] }
104): Promise<SpawnResult> {
105  const gradlew = resolveGradleWPath(projectRoot);
106  if (port != null) args.push(getPortArg(port));
107  Log.debug(`  ${gradlew} ${args.join(' ')}`);
108  try {
109    return await spawnAsync(gradlew, args, {
110      cwd: projectRoot,
111      stdio: 'inherit',
112    });
113  } catch (error: any) {
114    // User aborted the command with ctrl-c
115    if (error.status === 130) {
116      // Fail silently
117      throw new AbortCommandError();
118    }
119    throw error;
120  }
121}
122