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