From 343f9f214da767c4e00e5f8bf08b6ec56b3391e7 Mon Sep 17 00:00:00 2001 From: Luca Micieli Date: Tue, 17 Oct 2023 15:51:58 +0200 Subject: [PATCH] feat: run android build directly from build folder --- README.md | 2 +- packages/cli/src/application/androidUtils.ts | 26 ++++---- packages/cli/src/application/buildIos.ts | 1 - .../src/application/cloud/buildsManagement.ts | 3 +- packages/cli/src/application/runAndroid.ts | 26 ++++---- packages/cli/src/application/runIos.ts | 1 - packages/cli/src/application/utils.ts | 60 +++++++++++++------ packages/cli/src/commands/makeBuildCurrent.ts | 10 +++- packages/cli/src/commands/run.ts | 14 ++--- 9 files changed, 87 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 83b66f0..9c6d85d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ repack it with the new JS bundle. For now, the developers should choose if the native build should be forced or not. In the future, the cli tool may add some heuristics to decide when to force the native build. -![Run android with build cached](./docs/assets/run-android-build-cached.png) +![Run android with build cached](./packages/cli/docs/assets/run-android-build-cached.png) ## Disclaimer diff --git a/packages/cli/src/application/androidUtils.ts b/packages/cli/src/application/androidUtils.ts index 209baa2..fc535d5 100644 --- a/packages/cli/src/application/androidUtils.ts +++ b/packages/cli/src/application/androidUtils.ts @@ -1,24 +1,30 @@ -import { getProjectRootDir, getRootDestinationFolder } from './utils'; -import path from 'path'; -import fs from 'fs'; +import { getBuildFolderByBuildId, getProjectRootDir, getRootDestinationFolder } from "./utils"; +import path from "path"; +import fs from "fs"; -export function getAppBuildFolder(flavorName?: string, release?: boolean) { - const buildType = release ? 'release' : 'debug'; +export function getAppBuildFolder(flavorName?: string, release?: boolean, buildId?: string) { + const buildType = release ? "release" : "debug"; + const baseBuildFolder = buildId ? getBuildFolderByBuildId(buildId) : getRootDestinationFolder(); - const appPath = `${getRootDestinationFolder()}/android/${flavorName ? `${flavorName}/` : 'default/'}${buildType}`; + const appPath = path.join( + baseBuildFolder, + 'android', + `${flavorName ? `${flavorName}` : "default"}`, + buildType + ); return appPath; } export function getAndroidIndexJsPath() { - const androidSpecific = path.join(getProjectRootDir(), 'index.android.js'); + const androidSpecific = path.join(getProjectRootDir(), "index.android.js"); if (fs.existsSync(androidSpecific)) { return androidSpecific; } else { - return path.join(getProjectRootDir(), 'index.js'); + return path.join(getProjectRootDir(), "index.js"); } } -export function checkBuildPresent(buildFlavor?: string, release?: boolean) { - const appPath = getAppBuildFolder(buildFlavor, release); +export function checkBuildPresent(buildFlavor?: string, release?: boolean, buildId?: string) { + const appPath = getAppBuildFolder(buildFlavor, release, buildId); return fs.existsSync(appPath); } diff --git a/packages/cli/src/application/buildIos.ts b/packages/cli/src/application/buildIos.ts index 4a993a7..2ef84a5 100644 --- a/packages/cli/src/application/buildIos.ts +++ b/packages/cli/src/application/buildIos.ts @@ -43,7 +43,6 @@ function _buildIos(buildType?: string, platform: IosPlatform = iosBuildPlatforms executeCommand(`mkdir -p ${destinationDir}`); executeCommand(`rm -rf ${destination}`); const copyCommand = `cp -a '${source}' '${destination}'`; - console.log(`Copying: ${copyCommand}`); executeCommand(copyCommand); return destination; } else { diff --git a/packages/cli/src/application/cloud/buildsManagement.ts b/packages/cli/src/application/cloud/buildsManagement.ts index 3b77256..377e38c 100644 --- a/packages/cli/src/application/cloud/buildsManagement.ts +++ b/packages/cli/src/application/cloud/buildsManagement.ts @@ -10,7 +10,7 @@ import { Build, RemoteStorage } from '@rn-buildhub/storage-interface'; async function zipFolder(folderPath: string, outputZipPath: string) { if (!fs.existsSync(folderPath)) { - console.error(`The folder "${folderPath}" does not exist.`); + logger.error(`The folder "${folderPath}" does not exist.`); return; } const newZip = new AdmZip(); @@ -65,7 +65,6 @@ function unzipFile(zipFilePath: string, destinationFolder: string): void { zip.extractAllTo(destinationFolder, true); - console.log(`File extracted to ${destinationFolder}`); } async function downloadZipBuild(buildInfo: Build, buildId: string, adapter: RemoteStorage) { diff --git a/packages/cli/src/application/runAndroid.ts b/packages/cli/src/application/runAndroid.ts index ae937d7..714203b 100644 --- a/packages/cli/src/application/runAndroid.ts +++ b/packages/cli/src/application/runAndroid.ts @@ -1,11 +1,10 @@ import childProcess, { execSync } from 'child_process'; import fs from 'fs'; -import { getAppName, getProjectRootDir, getRootDestinationFolder } from './utils'; +import { getAppName, getProjectRootDir, getRootDestinationFolder, launchEmulator } from "./utils"; import listAndroidDevices from '@react-native-community/cli-platform-android/build/commands/runAndroid/listAndroidDevices'; import tryRunAdbReverse from '@react-native-community/cli-platform-android/build/commands/runAndroid/tryRunAdbReverse'; import adb from '@react-native-community/cli-platform-android/build/commands/runAndroid/adb'; import _getAdbPath from '@react-native-community/cli-platform-android/build/commands/runAndroid/getAdbPath'; -import tryLaunchEmulator from '@react-native-community/cli-platform-android/build/commands/runAndroid/tryLaunchEmulator'; import { buildAndroid } from './buildAndroid'; import { checkBuildPresent, getAppBuildFolder } from './androidUtils'; import path from 'path'; @@ -48,8 +47,8 @@ export function findBestApkInFolder(dir: string, arc?: string) { } } -function installApp(device: string, engineDir: string, buildType?: string) { - const appDir = getAppBuildFolder(buildType); +function installApp(device: string, engineDir: string, buildType?: string, buildId?:string) { + const appDir = getAppBuildFolder(buildType, false, buildId); const cpu = adb.getCPU(getAdbPath(), device); @@ -85,18 +84,21 @@ function getBundleIdentifier(appBuildFolder: string): string { return 'todo'; } -function installAndLaunch(port: string, deviceId: string, buildType: string | undefined, appIdentifier: string) { +function installAndLaunch(port: string, deviceId: string, buildType: string | undefined, appIdentifier: string, buildId?: string) { tryRunAdbReverse(port, deviceId); logger.info('Installing app...'); - installApp(deviceId, getProjectRootDir(), buildType); + installApp(deviceId, getProjectRootDir(), buildType, buildId); logger.info('Launching app...'); launchApp(deviceId, appIdentifier); } -export async function runApp(buildFlavor?: string, port = '8081', forceBuild?: boolean, buildId: string = 'local') { - if (forceBuild || !checkBuildPresent(buildFlavor)) { +export async function runApp(buildFlavor?: string, port = '8081', forceBuild?: boolean, buildId?: string) { + if (forceBuild || !checkBuildPresent(buildFlavor,false, buildId)) { logger.info('Build not present, starting build'); + if(buildId) { + throw new Error(`The requested build id ${buildId} does not contain an android build for flavor ${buildFlavor}`); + } await buildAndroid(buildFlavor); } else { logger.info('Build already present, skipping build'); @@ -105,19 +107,19 @@ export async function runApp(buildFlavor?: string, port = '8081', forceBuild?: b // todo improvement: if there is only one device, use it directly const device = await listAndroidDevices(); - const appIdentifier = getBundleIdentifier(getAppBuildFolder(buildFlavor)); + const appIdentifier = getBundleIdentifier(getAppBuildFolder(buildFlavor, false, buildId)); if (!device) { throw new Error('No android devices available'); } else { if (device.connected) { - installAndLaunch(port, device.deviceId!, buildFlavor, appIdentifier); + installAndLaunch(port, device.deviceId!, buildFlavor, appIdentifier, buildId); } else { const newEmulatorPort = await getAvailableDevicePort(); const emulator = `emulator-${newEmulatorPort}`; - const result = await tryLaunchEmulator(getAdbPath(), device.readableName, newEmulatorPort); + const result = await launchEmulator(getAdbPath(), device.readableName, newEmulatorPort, emulator); if (result.success) { - installAndLaunch(port, emulator, buildFlavor, appIdentifier); + installAndLaunch(port, emulator, buildFlavor, appIdentifier, buildId); } } } diff --git a/packages/cli/src/application/runIos.ts b/packages/cli/src/application/runIos.ts index 8a063c1..d79ee88 100644 --- a/packages/cli/src/application/runIos.ts +++ b/packages/cli/src/application/runIos.ts @@ -35,7 +35,6 @@ function checkBuildPresent(buildType: string, target: any) { function installApp(deviceUdid: string, buildType: string, target: any) { const { destination } = getIosBuildDestination(target, buildType); const res = childProcess.execSync(`xcrun simctl install ${deviceUdid} ${destination}`, { encoding: 'utf-8' }); - console.log('res', res); } function launchApp(deviceUid: string, bundleId: string) { diff --git a/packages/cli/src/application/utils.ts b/packages/cli/src/application/utils.ts index a2a4598..77ff1dd 100644 --- a/packages/cli/src/application/utils.ts +++ b/packages/cli/src/application/utils.ts @@ -1,19 +1,22 @@ -import process from 'process'; -import fs from 'fs'; -import path from 'path'; -import { execSync, ExecSyncOptions, ExecSyncOptionsWithBufferEncoding } from 'child_process'; -import { ProjectConfiguration } from './cloud/projectsManagement'; -import { RemoteStorage } from '@rn-buildhub/storage-interface'; -import util from 'util'; - -const execAsync = util.promisify(require('child_process').exec); +import process from "process"; +import fs from "fs"; +import path from "path"; +import { execSync, ExecSyncOptions, ExecSyncOptionsWithBufferEncoding } from "child_process"; +import { ProjectConfiguration } from "./cloud/projectsManagement"; +import { RemoteStorage } from "@rn-buildhub/storage-interface"; +import util from "util"; +import tryLaunchEmulator + from "@react-native-community/cli-platform-android/build/commands/runAndroid/tryLaunchEmulator"; +import getAdbPath from "@react-native-community/cli-platform-android/build/commands/runAndroid/getAdbPath"; + +const execAsync = util.promisify(require("child_process").exec); export function executeCommand(command: string, options?: ExecSyncOptionsWithBufferEncoding) { - execSync(command, { stdio: 'inherit', ...(options || {}) }); + execSync(command, { stdio: "inherit", ...(options || {}) }); } export function executeCommandWithOutPut(command: string, options?: ExecSyncOptions) { - return execSync(command, { encoding: 'utf8', ...(options || {}) }) as string; + return execSync(command, { encoding: "utf8", ...(options || {}) }) as string; } export function executeCommandAsync(command: string, options?: ExecSyncOptionsWithBufferEncoding) { @@ -21,11 +24,11 @@ export function executeCommandAsync(command: string, options?: ExecSyncOptionsWi } export function getRootDestinationFolder() { - return path.join(getProjectRootDir(), '.rn-build-hub'); + return path.join(getProjectRootDir(), ".rn-build-hub"); } export function getConfigFile() { - return path.join(getProjectRootDir(), '.rn-build-hub.json'); + return path.join(getProjectRootDir(), ".rn-build-hub.json"); } export function getProjectRootDir() { @@ -36,7 +39,7 @@ export function getProjectRootDir() { export function getAppName() { const projectRootDir = getProjectRootDir(); - const packageJson = JSON.parse(fs.readFileSync(path.join(projectRootDir, 'package.json'), 'utf8')); + const packageJson = JSON.parse(fs.readFileSync(path.join(projectRootDir, "package.json"), "utf8")); // todo default app name improve passing from command line return packageJson.name; } @@ -46,19 +49,38 @@ export function sleep(ms: number) { } export function getRootModuleDir() { - return path.join(__dirname, '..', '..'); + return path.join(__dirname, "..", ".."); } export function getApkToolExecutable() { - return path.join(getRootModuleDir(), 'apktool'); + return path.join(getRootModuleDir(), "apktool"); } export function getUberSignJava() { - return path.join(getRootModuleDir(), 'uber-apk-signer.jar'); + return path.join(getRootModuleDir(), "uber-apk-signer.jar"); +} + +function waitFormEmulatorBoot(emulatorName: string): Promise { + const output = execSync(`${getAdbPath()} -s ${emulatorName} shell getprop sys.boot_completed`); + if (output.toString().trim() !== "1") { + return sleep(1000).then(() => waitFormEmulatorBoot(emulatorName)); + } else { + return Promise.resolve(); + } +} + +export async function launchEmulator(adbPath: string, emulatorName: string, port: number, emulatorId:string) { + const result = await tryLaunchEmulator(adbPath, emulatorName, port); + if (!result.success) { + throw new Error("Unable to launch emulator"); + } else { + await waitFormEmulatorBoot(emulatorId); + return result; + } } export function getBuildFolderByBuildId(buildId: string) { - return path.join(getRootDestinationFolder(), 'builds', buildId); + return path.join(getRootDestinationFolder(), "builds", buildId); } export function capitalize(str: string) { @@ -67,7 +89,7 @@ export function capitalize(str: string) { export function getRemoteStorage(config: ProjectConfiguration): RemoteStorage { if (!config.remote.name) { - throw new Error('Remote adapter name is required'); + throw new Error("Remote adapter name is required"); } try { const connectorPackage = require(config.remote.name); diff --git a/packages/cli/src/commands/makeBuildCurrent.ts b/packages/cli/src/commands/makeBuildCurrent.ts index 31d68c1..f21f2c9 100644 --- a/packages/cli/src/commands/makeBuildCurrent.ts +++ b/packages/cli/src/commands/makeBuildCurrent.ts @@ -6,10 +6,11 @@ import logger from '../application/logger'; import { getBuildFolderByBuildId } from '../application/utils'; import { ProjectConfiguration, updateCurrentBuildInFile } from '../application/cloud/projectsManagement'; -export async function updateCurrentBuild(buildId: string, config: ProjectConfiguration) { +type RequestedBuildId = 'last' | string & {}; +export async function downloadBuildIfNotPresent(buildId: RequestedBuildId, config: ProjectConfiguration) { let buildIdToDownload: string; - if (buildId === 'last') { + if (buildId === "last") { const buildId = await getLastBuild(config); buildIdToDownload = buildId; } else { @@ -21,6 +22,11 @@ export async function updateCurrentBuild(buildId: string, config: ProjectConfigu } else { await downloadBuild(buildIdToDownload, config); } + return buildIdToDownload; +} + +export async function updateCurrentBuild(requestedBuildId: RequestedBuildId, config: ProjectConfiguration) { + let buildIdToDownload = await downloadBuildIfNotPresent(requestedBuildId, config); await makeCurrentBuild(buildIdToDownload); diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 4fc514f..8c1e2d1 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -4,7 +4,7 @@ import { runApp as runIos } from '../application/runIos'; import { startMetro, checkIsMetroRunning } from '../application/metroManager'; import logger from '../application/logger'; import { iosBuildPlatforms } from '../application/iosUtils'; -import { updateCurrentBuild } from './makeBuildCurrent'; +import { downloadBuildIfNotPresent, updateCurrentBuild } from "./makeBuildCurrent"; import RemoteAwareCommand from '../_projectAwareCommand'; export default class Run extends RemoteAwareCommand { @@ -18,7 +18,7 @@ export default class Run extends RemoteAwareCommand { flavor: Flags.string({ char: 'f', description: 'Specify the android flavor or the ios scheme to build' }), verbose: Flags.boolean({ description: 'Verbose output' }), forceBuild: Flags.boolean({ aliases: ['fb', 'force-build'], description: 'Force a native rebuild' }), - buildId: Flags.string({ description: 'Specify the build id. Can be local, last or a buildId', default: 'local' }), + buildId: Flags.string({ description: 'Specify the build id. Can be local, last or a buildId', default: undefined }), }; static args = { @@ -32,17 +32,15 @@ export default class Run extends RemoteAwareCommand { const shouldRunIos = flags.ios ?? flags.all; const buildFlavor = flags.flavor; const forceBuild = flags.forceBuild; - const buildId = flags.buildId; + let buildId = flags.buildId; logger.setVerbose(flags.verbose); const start = performance.now(); logger.info('Checking if metro is running...'); - if (buildId !== 'local') { + if (buildId) { logger.info(`Requested to run specific id ${buildId}`); - // todo remote command only if needed? - // do we have to copy to local? can't we just run from build folder? - await updateCurrentBuild(buildId, this.currentProject); + buildId = await downloadBuildIfNotPresent(buildId, this.currentProject); } const isMetroRunning = await checkIsMetroRunning(); @@ -56,7 +54,7 @@ export default class Run extends RemoteAwareCommand { if (shouldRunAndroid) { logger.info(`Running android app ${buildFlavor ? `with flavor ${buildFlavor}` : ''}`); - await runAndroid(buildFlavor, undefined, forceBuild); + await runAndroid(buildFlavor, undefined, forceBuild, buildId); } if (shouldRunIos) { logger.info(`Running ios app ${buildFlavor ? `with flavor ${buildFlavor}` : ''}`);