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

Introduce apks support #367

Merged
merged 13 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from 4 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
11 changes: 6 additions & 5 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import semver from 'semver';

const ZIP_MAGIC = 'PK';
const rootDir = path.resolve(__dirname, process.env.NO_PRECOMPILE ? '..' : '../..');
const APKS_EXTENSION = '.apks';

/**
* @typedef {Object} PlatformInfo
Expand Down Expand Up @@ -61,15 +62,15 @@ async function getAndroidPlatformAndPath () {
return result;
}

async function unzipFile (zipPath) {
log.debug(`Unzipping ${zipPath}`);
async function unzipFile (zipPath, dstRoot = path.dirname(zipPath)) {
log.debug(`Unzipping '${zipPath}' to '${dstRoot}'`);
try {
await assertZipArchive(zipPath);
if (system.isWindows()) {
await zip.extractAllTo(zipPath, path.dirname(zipPath));
await zip.extractAllTo(zipPath, dstRoot);
log.debug("Unzip successful");
} else {
await exec('unzip', ['-o', zipPath], {cwd: path.dirname(zipPath)});
await exec('unzip', ['-o', zipPath], {cwd: dstRoot});
log.debug("Unzip successful");
}
} catch (e) {
Expand Down Expand Up @@ -381,5 +382,5 @@ export {
getIMEListFromOutput, getJavaForOs, isShowingLockscreen, isCurrentFocusOnKeyguard,
getSurfaceOrientation, isScreenOnFully, buildStartCmd, getJavaHome,
rootDir, getSdkToolsVersion, getApksignerForOs, getBuildToolsDirs,
getApkanalyzerForOs, getOpenSslForOs, extractMatchingPermissions,
getApkanalyzerForOs, getOpenSslForOs, extractMatchingPermissions, APKS_EXTENSION,
};
14 changes: 14 additions & 0 deletions lib/tools/adb-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ methods.initZipAlign = async function () {
this.binaries.zipalign = await this.getSdkBinaryPath("zipalign");
};

/**
* Get the full path to bundletool binary and assign it to
* this.binaries.bundletool property
*/
methods.initBundletool = async function () {
try {
const bundletoolPath = await fs.which('bundletool.jar');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should export the path to 'bundletool.jar', right? 👀

Copy link
Contributor Author

@mykola-mokhnach mykola-mokhnach Oct 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. bundletool.jar binary should be in any of folders enumerated in system PATH

this.binaries.bundletool = bundletoolPath;
} catch (err) {
throw new Error('bundletool.jar binary is expected to be present in PATH. ' +
'Visit https://github.com/google/bundletool for more details.');
}
};

/**
* Retrieve the API level of the device under test.
*
Expand Down
64 changes: 41 additions & 23 deletions lib/tools/android-manifest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { exec } from 'teen_process';
import log from '../logger.js';
import { getAndroidPlatformAndPath, unzipFile, assertZipArchive,
getApkanalyzerForOs } from '../helpers.js';
import {
getAndroidPlatformAndPath, unzipFile, assertZipArchive,
getApkanalyzerForOs, APKS_EXTENSION } from '../helpers.js';
import { fs } from 'appium-support';
import _ from 'lodash';
import path from 'path';
Expand Down Expand Up @@ -148,20 +149,24 @@ async function extractApkInfoWithApkanalyzer (localApk, apkanalyzerPath) {
/**
* Extract package and main activity name from application manifest.
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to application .apk(s) package
* @return {APKInfo} The parsed application info.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
manifestMethods.packageAndLaunchActivityFromManifest = async function (localApk) {
manifestMethods.packageAndLaunchActivityFromManifest = async function (appPath) {
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

const apkInfoGetters = [
async () => {
const apkanalyzerPath = await getApkanalyzerForOs(this);
return await extractApkInfoWithApkanalyzer(localApk, apkanalyzerPath);
return await extractApkInfoWithApkanalyzer(appPath, apkanalyzerPath);
},
async () => {
await this.initAapt();
return await extractApkInfoWithApkTools(localApk,
return await extractApkInfoWithApkTools(appPath,
this.binaries.aapt, this.jars['appium_apk_tools.jar'], this.tmpDir);
},
];
Expand All @@ -175,29 +180,32 @@ manifestMethods.packageAndLaunchActivityFromManifest = async function (localApk)
return {apkPackage, apkActivity};
} catch (e) {
if (infoGetter !== _.last(apkInfoGetters)) {
log.info(`Using the alternative activity name detection method ` +
`because of: ${e.message}`);
log.info(`Using the alternative activity name detection method because of: ${e.message}`);
}
savedError = e;
}
}
throw new Error(`packageAndLaunchActivityFromManifest failed. ` +
`Original error: ${savedError.message}` +
throw new Error(`packageAndLaunchActivityFromManifest failed. Original error: ${savedError.message}` +
(savedError.stderr ? `; StdErr: ${savedError.stderr}` : ''));
};

/**
* Extract target SDK version from application manifest.
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to .apk(s) package.
* @return {number} The version of the target SDK.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
manifestMethods.targetSdkVersionFromManifest = async function (localApk) {
manifestMethods.targetSdkVersionFromManifest = async function (appPath) {
await this.initAapt();

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log.info("Extracting package and launch activity from manifest");
let args = ['dump', 'badging', localApk];
let args = ['dump', 'badging', appPath];
let output;
try {
let {stdout} = await exec(this.binaries.aapt, args);
Expand Down Expand Up @@ -301,32 +309,42 @@ manifestMethods.insertManifest = async function (manifest, srcApk, dstApk) {
/**
* Check whether package manifest contains Internet permissions.
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to .apk(s) package.
* @return {boolean} True if the manifest requires Internet access permission.
*/
manifestMethods.hasInternetPermissionFromManifest = async function (localApk) {
manifestMethods.hasInternetPermissionFromManifest = async function (appPath) {
await this.initAapt();
log.debug(`Checking if '${localApk}' requires internet access permission in the manifest`);

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log.debug(`Checking if '${appPath}' requires internet access permission in the manifest`);
try {
let {stdout} = await exec(this.binaries.aapt, ['dump', 'badging', localApk]);
let {stdout} = await exec(this.binaries.aapt, ['dump', 'badging', appPath]);
return new RegExp(/uses-permission:.*'android.permission.INTERNET'/).test(stdout);
} catch (e) {
throw new Error(`Cannot check if '${localApk}' requires internet access permission. ` +
throw new Error(`Cannot check if '${appPath}' requires internet access permission. ` +
`Original error: ${e.message}`);
}
};

/*
/**
* Prints out the manifest extracted from the apk
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to application package.
* @param {?string} logLevel - The level at which to log. E.g., 'debug'
*/
manifestMethods.printManifestFromApk = async function printManifestFromApk (localApk, logLevel = 'debug') {
manifestMethods.printManifestFromApk = async function printManifestFromApk (appPath, logLevel = 'debug') {
await this.initAapt();
log[logLevel](`Android manifest extracted from '${localApk}'`);

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log[logLevel](`Extracting the manifest from '${appPath}'`);
let out = false;
const {stdout} = await exec(this.binaries.aapt, ['l', '-a', localApk]);
const {stdout} = await exec(this.binaries.aapt, ['l', '-a', appPath]);
for (const line of stdout.split('\n')) {
if (!out && line.includes('Android manifest:')) {
out = true;
Expand Down
28 changes: 21 additions & 7 deletions lib/tools/apk-signing.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { exec } from 'teen_process';
import path from 'path';
import log from '../logger.js';
import { tempDir, system, mkdirp, fs, zip } from 'appium-support';
import { getJavaForOs, getApksignerForOs, getJavaHome, rootDir } from '../helpers.js';
import { getJavaForOs, getApksignerForOs, getJavaHome, rootDir, APKS_EXTENSION } from '../helpers.js';

const DEFAULT_PRIVATE_KEY = path.resolve(rootDir, 'keys', 'testkey.pk8');
const DEFAULT_CERTIFICATE = path.resolve(rootDir, 'keys', 'testkey.x509.pem');
const DEFAULT_CERT_DIGEST = 'a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc';
const BUNDLETOOL_TUTORIAL = 'https://developer.android.com/studio/command-line/bundletool';
const APKSIGNER_VERIFY_FAIL = 'DOES NOT VERIFY';

let apkSigningMethods = {};
Expand Down Expand Up @@ -165,10 +166,23 @@ apkSigningMethods.signWithCustomCert = async function (apk) {
* custom or default certificate based on _this.useKeystore_ property value
* and Zip-aligns it after signing.
*
* @param {string} apk - The full path to the local apk file.
* @param {string} appPath - The full path to the local .apk(s) file.
* @throws {Error} If signing fails.
*/
apkSigningMethods.sign = async function (apk) {
apkSigningMethods.sign = async function (appPath) {
if (appPath.endsWith(APKS_EXTENSION)) {
let message = 'Signing of .apks-files is not supported. ';
if (this.useKeystore) {
message += 'Consider manual application bundle signing with the custom keystore ' +
`like it is described at ${BUNDLETOOL_TUTORIAL}`;
} else {
message += `Consider manual application bundle signing with the key at '${DEFAULT_PRIVATE_KEY}' ` +
`and the certificate at '${DEFAULT_CERTIFICATE}'. Read ${BUNDLETOOL_TUTORIAL} for more details.`;
}
log.warn(message);
return;
}

let apksignerFound = true;
try {
await getApksignerForOs(this);
Expand All @@ -180,17 +194,17 @@ apkSigningMethods.sign = async function (apk) {
// it is necessary to apply zipalign only before signing
// if apksigner is used or only after signing if we only have
// sign.jar utility
await this.zipAlignApk(apk);
await this.zipAlignApk(appPath);
}

if (this.useKeystore) {
await this.signWithCustomCert(apk);
await this.signWithCustomCert(appPath);
} else {
await this.signWithDefaultCert(apk);
await this.signWithDefaultCert(appPath);
}

if (!apksignerFound) {
await this.zipAlignApk(apk);
await this.zipAlignApk(appPath);
}
};

Expand Down
Loading