UNPKG

appium-adb

Version:

Android Debug Bridge interface

140 lines (131 loc) 5.11 kB
import { log } from '../logger.js'; import path from 'path'; import { fs, tempDir, util } from '@appium/support'; import { LRUCache } from 'lru-cache'; import { unzipFile } from '../helpers.js'; import AsyncLock from 'async-lock'; import B from 'bluebird'; import crypto from 'crypto'; /** @type {LRUCache<string, string>} */ const AAB_CACHE = new LRUCache({ max: 10, dispose: (extractedFilesRoot) => fs.rimraf(/** @type {string} */ (extractedFilesRoot)), }); const AAB_CACHE_GUARD = new AsyncLock(); const UNIVERSAL_APK = 'universal.apk'; process.on('exit', () => { if (!AAB_CACHE.size) { return; } const paths = /** @type {string[]} */ ([...AAB_CACHE.values()]); log.debug(`Performing cleanup of ${paths.length} cached .aab ` + util.pluralize('package', paths.length)); for (const appPath of paths) { try { // Asynchronous calls are not supported in onExit handler fs.rimrafSync(appPath); } catch (e) { log.warn((/** @type {Error} */ (e)).message); } } }); /** * Builds a universal .apk from the given .aab package. See * https://developer.android.com/studio/command-line/bundletool#generate_apks * for more details. * * @this {import('../adb.js').ADB} * @param {string} aabPath Full path to the source .aab package * @param {import('./types').ApkCreationOptions} [opts={}] * @returns The path to the resulting universal .apk. The .apk is stored in the internal cache * by default. * @throws {Error} If there was an error while creating the universal .apk */ export async function extractUniversalApk (aabPath, opts = {}) { if (!await fs.exists(aabPath)) { throw new Error(`The file at '${aabPath}' either does not exist or is not accessible`); } const aabName = path.basename(aabPath); const apkName = aabName.substring(0, aabName.length - path.extname(aabName).length) + '.apk'; const tmpRoot = await tempDir.openDir(); const tmpApksPath = path.join(tmpRoot, `${aabName}.apks`); try { return await AAB_CACHE_GUARD.acquire(aabPath, async () => { const aabHash = await fs.hash(aabPath); const { keystore, keystorePassword, keyAlias, keyPassword, } = opts; let cacheHash = aabHash; if (keystore) { if (!await fs.exists(keystore)) { throw new Error(`The keystore file at '${keystore}' either does not exist ` + `or is not accessible`); } if (!keystorePassword || !keyAlias || !keyPassword) { throw new Error('It is mandatory to also provide keystore password, key alias, ' + 'and key password if the keystore path is set'); } const keystoreHash = await fs.hash(keystore); const keyAliasHash = crypto.createHash('sha1'); keyAliasHash.update(keyAlias); cacheHash = [cacheHash, keystoreHash, keyAliasHash.digest('hex')].join(':'); } log.debug(`Calculated the cache key for '${aabPath}': ${cacheHash}`); if (AAB_CACHE.has(cacheHash)) { const resultPath = path.resolve(/** @type {string} */ (AAB_CACHE.get(cacheHash)), apkName); if (await fs.exists(resultPath)) { return resultPath; } AAB_CACHE.delete(cacheHash); } await this.initAapt2(); const args = [ 'build-apks', '--aapt2', (/** @type {import('./types').StringRecord} */ (this.binaries)).aapt2, '--bundle', aabPath, '--output', tmpApksPath, ...(keystore ? [ '--ks', keystore, '--ks-pass', `pass:${keystorePassword}`, '--ks-key-alias', keyAlias, '--key-pass', `pass:${keyPassword}`, ] : []), '--mode=universal' ]; log.debug(`Preparing universal .apks bundle from '${aabPath}'`); await this.execBundletool(args, `Cannot build a universal .apks bundle from '${aabPath}'`); log.debug(`Unpacking universal application bundle at '${tmpApksPath}' to '${tmpRoot}'`); await unzipFile(tmpApksPath, tmpRoot); let universalApkPath; const fileDeletionPromises = []; const allFileNames = await fs.readdir(tmpRoot); for (const fileName of allFileNames) { const fullPath = path.join(tmpRoot, fileName); if (fileName === UNIVERSAL_APK) { universalApkPath = fullPath; } else { fileDeletionPromises.push(fs.rimraf(fullPath)); } } try { await B.all(fileDeletionPromises); } catch {} if (!universalApkPath) { log.debug(`The following items were extracted from the .aab bundle: ${allFileNames}`); throw new Error(`${UNIVERSAL_APK} cannot be found in '${aabPath}' bundle. ` + `Does the archive contain a valid application bundle?`); } const resultPath = path.join(tmpRoot, apkName); log.debug(`Found ${UNIVERSAL_APK} at '${universalApkPath}'. Caching it to '${resultPath}'`); await fs.mv(universalApkPath, resultPath); AAB_CACHE.set(cacheHash, tmpRoot); return resultPath; }); } catch (e) { await fs.rimraf(tmpRoot); throw e; } }