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