@bubblewrap/core
Version:
Core Library to generate, build and sign TWA projects
232 lines (231 loc) • 9.7 kB
JavaScript
;
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidSdkTools = exports.BUILD_TOOLS_VERSION = void 0;
const fs = require("fs");
const path = require("path");
const util = require("../util");
const Log_1 = require("../../lib/Log");
const Result_1 = require("../../lib/Result");
const ValidatePathError_1 = require("../errors/ValidatePathError");
exports.BUILD_TOOLS_VERSION = '34.0.0';
/**
* Wraps functionality of the Android SDK Tools and allows them to be invoked programatically.
*/
class AndroidSdkTools {
static async create(process, config, jdkHelper, log = new Log_1.ConsoleLog('AndroidSdkTools')) {
// unwrap will throw an error in case that the the path is valid and else will do nothing.
(await AndroidSdkTools.validatePath(config.androidSdkPath)).unwrap();
try {
return new AndroidSdkTools(process, config, jdkHelper, log);
}
catch (error) {
if (typeof error === 'string') {
throw new Error(error);
}
else {
throw error;
}
}
}
/**
* Constructs a new instance of AndroidSdkTools.
*
* @param {NodeJS.Process} process information from the OS process
* @param {Config} config the bubblewrap general configuration
* @param {jdkHelper} jdkHelper the JDK information to be used by the Android SDK
*/
constructor(process, config, jdkHelper, log = new Log_1.ConsoleLog('AndroidSdkTools')) {
this.log = log;
this.process = process;
this.config = config;
this.jdkHelper = jdkHelper;
if (this.process.platform === 'win32') {
this.pathJoin = path.win32.join;
}
else {
this.pathJoin = path.posix.join;
}
}
/**
* Installs the build tools into the the Android SDK. Equivalent to running
*
* `tools/bin/sdkmanager --install "build-tools;29.0.2"`
*/
async installBuildTools() {
const env = this.getEnv();
// The escape char allows us to pass a directory with spaces as sdk_root. On Windows,
// those are not properly handled by the Android SDK, so we try running without wrapping the
// value.
// TODO(andreban): Check for spaces in the path and throw an Error if one is found.
let sdkRootEscapeChar = '"';
let sdkManagerPath = this.pathJoin(this.getAndroidHome(), '/tools/bin/sdkmanager');
if (this.process.platform === 'win32') {
sdkRootEscapeChar = '';
sdkManagerPath += '.bat';
}
if (!fs.existsSync(sdkManagerPath)) {
// Android SDK version `6858069` and above doesn't have a `tools` folder anymore.
sdkManagerPath = this.pathJoin(this.getAndroidHome(), '/bin/sdkmanager');
if (this.process.platform === 'win32') {
sdkManagerPath += '.bat';
}
if (!fs.existsSync(sdkManagerPath)) {
throw new Error(`Could not find sdkmanager at: ${sdkManagerPath}`);
}
}
this.log.info('Installing Build Tools');
await util.execInteractive(sdkManagerPath, ['--install',
`"build-tools;${exports.BUILD_TOOLS_VERSION}"`,
// setting ANDROID_HOME via this.getEnv() should be enough, but version 6200805 of the
// the Android Command Line tools don't work properly if sdk_root is not set.
`--sdk_root=${sdkRootEscapeChar}${this.getAndroidHome()}${sdkRootEscapeChar}`], env);
}
/**
* Verifies if the build-tools are installed on the Android SDK.
*/
async checkBuildTools() {
const buildToolsPath = this.pathJoin(this.getAndroidHome(), '/build-tools/', exports.BUILD_TOOLS_VERSION);
return fs.existsSync(buildToolsPath);
}
/**
* Returns the path to the Android SDK.
* @returns {string} the path to the Android SDK.
*/
getAndroidHome() {
return this.pathJoin(this.config.androidSdkPath, '/');
}
/**
* Creates a Node Process with the correct ANDROID_HOME information
* @returns {NodeJS.ProcessEnv} the env with ANDROID_HOME set
*/
getEnv() {
const env = this.jdkHelper.getEnv();
env['ANDROID_HOME'] = this.getAndroidHome();
return env;
}
/**
* Invokes the zipalign tool from the Android SDK with the following flags:
* -f : overwrite existing outfile.zip.
* -v : verbose output.
* -p 4 : align all libraries to the 32-bit page boundary.
* More information on zipalign can be found here:
* https://developer.android.com/studio/command-line/zipalign
* @param {string} input path to the input file.
* @param {string} output path to the output file.
*/
async zipalign(input, output) {
const env = this.getEnv();
const zipalignCmd = [
`"${this.pathJoin(this.getAndroidHome(), `/build-tools/${exports.BUILD_TOOLS_VERSION}/zipalign`)}"`,
'-v -f -p 4',
input,
output,
];
await util.execute(zipalignCmd, env);
}
/**
* Invokes the zipalign tool from the Android SDK with the following flags:
* -c : confirm the alignment of the given file.
* -v : verbose output.
* -p 4 : align all libraries to the 32-bit page boundary.
* More information on zipalign can be found here:
* https://developer.android.com/studio/command-line/zipalign
* @param {string} input path to the input file.
*/
async zipalignOnlyVerification(input) {
const env = this.getEnv();
const zipalignCmd = [
`"${this.pathJoin(this.getAndroidHome(), `/build-tools/${exports.BUILD_TOOLS_VERSION}/zipalign`)}"`,
'-v -c -p 4',
input,
];
await util.execute(zipalignCmd, env);
}
/**
* Signs an Android APK, with they keystore
* @param {string} keystore path to the keystore
* @param {string} ksPass keystore password
* @param {string} alias key alias
* @param {string} keyPass key password
* @param {string} input path to the input APK file
* @param {string} output path where the signed APK will be generated
*/
async apksigner(keystore, ksPass, alias, keyPass, input, output) {
const env = this.getEnv();
const apkSignerParams = [
'sign',
'--ks', keystore,
'--ks-key-alias', alias,
'--ks-pass', `pass:${ksPass}`,
'--key-pass', `pass:${keyPass}`,
'--out', output,
input,
];
// This is a workaround for https://issuetracker.google.com/issues/150888434, where
// find_java.bat is unable to find the java command on Windows.
// We run apksigner.jar directly instead of invoking the bat.
if (this.process.platform === 'win32') {
const javaCmd = [
'-Xmx1024M',
'-Xss1m',
'-jar',
this.pathJoin(this.getAndroidHome(), `/build-tools/${exports.BUILD_TOOLS_VERSION}/lib/apksigner.jar`),
];
javaCmd.push(...apkSignerParams);
await this.jdkHelper.runJava(javaCmd);
return;
}
const apksignerCmd = this.pathJoin(this.getAndroidHome(), `/build-tools/${exports.BUILD_TOOLS_VERSION}/apksigner`);
await util.executeFile(apksignerCmd, apkSignerParams, env);
}
/**
* Installs an APK on an a device connected to the computer.
* @param apkFilePath the path to the APK to be installed
*/
async install(apkFilePath, passthroughArgs = []) {
if (!fs.existsSync(apkFilePath)) {
throw new Error(`Could not find APK file at ${apkFilePath}`);
}
const env = this.getEnv();
const installCmd = [
`"${this.pathJoin(this.getAndroidHome(), '/platform-tools/adb')}"`,
'install',
'-r', // Replace app if another with the same package id already installed.
...passthroughArgs,
apkFilePath,
];
await util.execute(installCmd, env, this.log);
}
/**
* Checks if `sdkPath` is valid.
* @param {string} sdkPath the path to the sdk.
*/
static async validatePath(sdkPath) {
const toolsPath = path.join(sdkPath, 'tools');
const binPath = path.join(sdkPath, 'bin');
// Checks if the path provided is valid. Older versions of the the Android SDK add the
// initial files inside the `tools` folder. Version `6858069` and above add it directly
// to the `bin` folder.
if (!fs.existsSync(sdkPath) || (!fs.existsSync(toolsPath)) && !fs.existsSync(binPath)) {
return Result_1.Result.error(new ValidatePathError_1.ValidatePathError('The provided androidSdk isn\'t correct.', 'PathIsNotCorrect'));
}
;
return Result_1.Result.ok(sdkPath);
}
}
exports.AndroidSdkTools = AndroidSdkTools;