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