Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: install system-images #57

Merged
merged 7 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/commands/android/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inquirer from 'inquirer';
import path from 'path';
import os from 'os';
import path from 'path';

import {AvailableOptions, SdkBinary} from './interfaces';
import {AvailableSubcommands} from './subcommands/interfaces';
Expand Down Expand Up @@ -77,13 +77,20 @@ export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = {
usageHelp: 'device_id'
}
]
},
{
name: 'system-image',
description: 'Install a system image'
}
]
},
uninstall: {
description: 'todo item',
description: 'Uninstall system images, AVDs, or apps from a device',
flags: [
{name: 'avd', description: 'todo item'},
{
name: 'avd',
description: 'Delete an Android Virtual Device'
},
{
name: 'app',
description: 'Uninstall an APK from a device',
Expand Down Expand Up @@ -162,3 +169,4 @@ export const BINARY_TO_PACKAGE_NAME: Record<SdkBinary | typeof NIGHTWATCH_AVD, s
emulator: 'emulator',
[NIGHTWATCH_AVD]: `system-images;android-30;google_apis;${ABI}`
};

42 changes: 10 additions & 32 deletions src/commands/android/dotcommands.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import colors from 'ansi-colors';
import {spawnSync} from 'child_process';
import * as dotenv from 'dotenv';
import path from 'path';

import {ANDROID_DOTCOMMANDS} from '../../constants';
import Logger from '../../logger';
import {getPlatformName} from '../../utils';
import {Platform, SdkBinary} from './interfaces';
import {checkJavaInstallation, getBinaryLocation, getBinaryNameForOS, getSdkRootFromEnv} from './utils/common';
import {showMissingBinaryHelp} from './subcommands/common';
import {checkJavaInstallation, getBinaryLocation, getSdkRootFromEnv} from './utils/common';
import {spawnCommandSync} from './utils/sdk';

export class AndroidDotCommand {
dotcmd: string;
Expand Down Expand Up @@ -55,42 +56,19 @@ export class AndroidDotCommand {
}
this.sdkRoot = sdkRootEnv;

return this.executeDotCommand();
}

loadEnvFromDotEnv(): void {
this.androidHomeInGlobalEnv = 'ANDROID_HOME' in process.env;
dotenv.config({path: path.join(this.rootDir, '.env')});
}

buildCommand(): string {
const binaryName = this.dotcmd.split('.')[1] as SdkBinary;
const binaryLocation = getBinaryLocation(this.sdkRoot, this.platform, binaryName, true);
if (!binaryLocation) {
showMissingBinaryHelp(binaryName);

let cmd: string;
if (binaryLocation === 'PATH') {
const binaryFullName = getBinaryNameForOS(this.platform, binaryName);
cmd = `${binaryFullName}`;
} else {
const binaryFullName = path.basename(binaryLocation);
const binaryDirPath = path.dirname(binaryLocation);
cmd = path.join(binaryDirPath, binaryFullName);
return false;
}

return cmd;
return spawnCommandSync(binaryLocation, binaryName, this.platform, this.args);
}

executeDotCommand(): boolean {
const cmd = this.buildCommand();
const result = spawnSync(cmd, this.args, {stdio: 'inherit'});

if (result.error) {
console.error(result.error);

return false;
}

return result.status === 0;
loadEnvFromDotEnv(): void {
this.androidHomeInGlobalEnv = 'ANDROID_HOME' in process.env;
dotenv.config({path: path.join(this.rootDir, '.env')});
}
}

29 changes: 29 additions & 0 deletions src/commands/android/subcommands/apiLevelNames.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"android-10": "Gingerbread (v2.3.3)",
"android-11": "Honeycomb (v3.0)",
"android-12": "Honeycomb (v3.1)",
"android-13": "Honeycomb (v3.2)",
"android-14": "Ice Cream Sandwich (v4.0)",
"android-15": "Ice Cream Sandwich (v4.0.3)",
"android-16": "Jelly Bean (v4.1)",
"android-17": "Jelly Bean (v4.2)",
"android-18": "Jelly Bean (v4.3)",
"android-19": "KitKat (v4.4)",
"android-20": "KitKat Watch (v4.4W)",
"android-21": "Lollipop (v5.0)",
"android-22": "Lollipop (v5.1)",
"android-23": "Marshmallow (v6.0)",
"android-24": "Nougat (v7.0)",
"android-25": "Nougat (v7.1)",
"android-26": "Oreo (v8.0)",
"android-27": "Oreo (v8.1)",
"android-28": "Pie (v9.0)",
"android-29": "Android 10",
"android-30": "Android 11",
"android-31": "Android 12",
"android-32": "Android 12L",
"android-33": "Android 13",
"android-34": "Android 14",
"android-35": "Android 15"
}

17 changes: 9 additions & 8 deletions src/commands/android/subcommands/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Options, Platform} from '../../interfaces';
import {verifyOptions} from '../common';
import {installApp} from './app';
import {createAvd} from './avd';
import {installSystemImage} from './system-image';

export async function install(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
const optionsVerified = verifyOptions('install', options);
Expand All @@ -19,6 +20,8 @@ export async function install(options: Options, sdkRoot: string, platform: Platf

if (subcommandFlag === 'app') {
return await installApp(options, sdkRoot, platform);
} else if (subcommandFlag === 'system-image') {
return await installSystemImage(sdkRoot, platform);
} else if (subcommandFlag === 'avd') {
return await createAvd(sdkRoot, platform);
}
Expand All @@ -31,15 +34,13 @@ async function promptForFlag(): Promise<string> {
type: 'list',
name: 'flag',
message: 'Select what do you want to install:',
choices: ['APK', 'AVD']
choices: [
{name: 'Android app (APK)', value: 'app'},
{name: 'Android Virtual Device (AVD)', value: 'avd'},
{name: 'System image', value: 'system-image'}
]
});
Logger.log();

const flag = flagAnswer.flag;
if (flag === 'APK') {
return 'app';
}

return 'avd';
return flagAnswer.flag;
}

130 changes: 130 additions & 0 deletions src/commands/android/subcommands/install/system-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import colors from 'ansi-colors';
import inquirer from 'inquirer';

import Logger from '../../../../logger';
import {Platform} from '../../interfaces';
import {getBinaryLocation} from '../../utils/common';
import {execBinarySync, spawnCommandSync} from '../../utils/sdk';
import apiLevelNames from '../apiLevelNames.json';
import {showMissingBinaryHelp} from '../common';
import {ApiLevelNames, AvailableSystemImages} from '../interfaces';

export async function installSystemImage(sdkRoot: string, platform: Platform): Promise<boolean> {
try {
const sdkmanagerLocation = getBinaryLocation(sdkRoot, platform, 'sdkmanager', true);
if (!sdkmanagerLocation) {
showMissingBinaryHelp('sdkmanager');

return false;
}

const stdout = execBinarySync(sdkmanagerLocation, 'sdkmanager', platform, '--list');
if (!stdout) {
Logger.log(`${colors.red('\nFailed to fetch available system images!')} Please try again.\n`);

return false;
}

// `sdkmanager --list` output have repetitive system image names in different sections
// (Installed packages, Available packages, Available updates, etc.)
//
// Parse the output and store the system image names in a Set to avoid duplicates.
const availableImageNames = new Set<string>();

// Before parsing and removing duplicates, sort the system images
// to get them in increasing order of API level.
const lines = stdout.split('\n').sort();

lines.forEach(line => {
if (!line.includes('system-images;')) {
return;
}

const imageName = line.split('|')[0].trim();
availableImageNames.add(imageName);
});

// System images are represented in the format: system-images;android-<api-level>;<type>;<arch>
// Group all the system image types by API level. Group all the architectures by system image type.
const availableSystemImages: AvailableSystemImages = {};
availableImageNames.forEach(imageName => {
if (!imageName.includes('system-image')) {
return;
}
const imageSplit = imageName.split(';');
const apiLevel = imageSplit[1];
const type = imageSplit[2];
const arch = imageSplit[3];

availableSystemImages[apiLevel] ||= [];

const imageType = availableSystemImages[apiLevel].find(image => image.type === type);
if (!imageType) {
availableSystemImages[apiLevel].push({
type: type,
archs: [arch]
});
} else {
imageType.archs.push(arch);
}
});

// We've got the available system images grouped by API level.
// Now, prompt the user to select the API level, system image type, and architecture.
const apiLevelChoices = Object.keys(availableSystemImages).map(apiLevel => {
let name = apiLevel;
if ((apiLevelNames as ApiLevelNames)[apiLevel]) {
name = `${apiLevel}: ${(apiLevelNames as ApiLevelNames)[apiLevel]}`;
}

return {name, value: apiLevel};
});

const apiLevelAnswer = await inquirer.prompt({
type: 'list',
name: 'apiLevel',
message: 'Select the API level for system image:',
choices: apiLevelChoices
});
const apiLevel = apiLevelAnswer.apiLevel;

const systemImageTypeAnswer = await inquirer.prompt({
type: 'list',
name: 'systemImageType',
message: `Select the system image type for ${colors.cyan(apiLevel)}:`,
choices: availableSystemImages[apiLevel].map(image => image.type)
});
const type = systemImageTypeAnswer.systemImageType;

const systemImageArchAnswer = await inquirer.prompt({
type: 'list',
name: 'systemImageArch',
message: 'Select the architecture for the system image:',
choices: availableSystemImages[apiLevel].find(image => image.type === type)?.archs
});
const arch = systemImageArchAnswer.systemImageArch;

const systemImageName = `system-images;${apiLevel};${type};${arch}`;

Logger.log();
Logger.log(`Installing system image: ${colors.cyan(systemImageName)}\n`);

const installationStatus = spawnCommandSync(sdkmanagerLocation, 'sdkmanager', platform, [systemImageName]);
if (installationStatus) {
Logger.log(colors.green('\nSystem image installed successfully!\n'));

return true;
}

Logger.log(colors.red('\nSomething went wrong while installing system image.\n'));
Logger.log(`To verify if the system image was installed, run: ${colors.cyan('npx @nightwatch/mobile-helper android.sdkmanager --list_installed')}`);
Logger.log('If the system image is not found listed, please try installing again.\n');

return false;
} catch (error) {
Logger.log(colors.red('\nError occurred while installing system image.'));
console.error(error);

return false;
}
}
12 changes: 12 additions & 0 deletions src/commands/android/subcommands/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ export interface SubcommandOptionsVerificationResult {
subcommandFlag: string;
configs: string[];
}

export interface AvailableSystemImages {
[apiLevel: string]: {
type: string;
archs: string[];
}[]
}

export interface ApiLevelNames {
[apiLevel: string]: string
}

2 changes: 1 addition & 1 deletion src/commands/android/subcommands/uninstall/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export async function uninstallApp(options: Options, sdkRoot: string, platform:

return false;
} catch (error) {
Logger.log(colors.red('Error occurred while uninstalling app.'));
Logger.log(colors.red('\nError occurred while uninstalling app.'));
console.error(error);

return false;
Expand Down
4 changes: 3 additions & 1 deletion src/commands/android/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const getBinaryNameForOS = (platform: Platform, binaryName: string) => {
return binaryName;
};

export const getBinaryLocation = (sdkRoot: string, platform: Platform, binaryName: SdkBinary, suppressOutput = false) => {
export const getBinaryLocation = (
sdkRoot: string, platform: Platform, binaryName: SdkBinary, suppressOutput = false
): string => {
const failLocations: string[] = [];

const binaryFullName = getBinaryNameForOS(platform, binaryName);
Expand Down
22 changes: 21 additions & 1 deletion src/commands/android/utils/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import colors from 'ansi-colors';
import {exec, execSync} from 'child_process';
import {exec, execSync, spawnSync} from 'child_process';
import fs from 'fs';
import {homedir} from 'os';
import path from 'path';
Expand Down Expand Up @@ -224,6 +224,26 @@ export const execBinaryAsync = (
});
};

export const spawnCommandSync = (binaryLocation: string, binaryName: string, platform: Platform, args: string[]) => {
let cmd: string;
if (binaryLocation === 'PATH') {
const binaryFullName = getBinaryNameForOS(platform, binaryName);
cmd = `${binaryFullName}`;
} else {
cmd = binaryLocation;
}

const result = spawnSync(cmd, args, {stdio: 'inherit'});

if (result.error) {
console.error(result.error);

return false;
}

return result.status === 0;
};

export const getBuildToolsAvailableVersions = (buildToolsPath: string): string[] => {
if (!fs.existsSync(buildToolsPath)) {
return [];
Expand Down
Loading