diff --git a/lib/common/http-client.ts b/lib/common/http-client.ts index 55ac67aa93..3219117377 100644 --- a/lib/common/http-client.ts +++ b/lib/common/http-client.ts @@ -1,6 +1,7 @@ import * as _ from "lodash"; import { EOL } from "os"; import * as util from "util"; +import { pipeline } from "stream/promises"; import { Server, IProxySettings, IProxyService } from "./declarations"; import { injector } from "./yok"; import axios from "axios"; @@ -18,12 +19,12 @@ export class HttpClient implements Server.IHttpClient { constructor( private $logger: ILogger, private $proxyService: IProxyService, - private $staticConfig: Config.IStaticConfig + private $staticConfig: Config.IStaticConfig, ) {} public async httpRequest( options: any, - proxySettings?: IProxySettings + proxySettings?: IProxySettings, ): Promise { try { const result = await this.httpRequestCore(options, proxySettings); @@ -43,7 +44,7 @@ export class HttpClient implements Server.IHttpClient { this.$logger.warn( "%s Retrying request to %s...", err.message, - options.url || options + options.url || options, ); const retryResult = await this.httpRequestCore(options, proxySettings); return { @@ -59,7 +60,7 @@ export class HttpClient implements Server.IHttpClient { private async httpRequestCore( options: any, - proxySettings?: IProxySettings + proxySettings?: IProxySettings, ): Promise { if (_.isString(options)) { options = { @@ -79,7 +80,7 @@ export class HttpClient implements Server.IHttpClient { cliProxySettings, options, headers, - requestProto + requestProto, ); if (!headers["User-Agent"]) { @@ -113,7 +114,11 @@ export class HttpClient implements Server.IHttpClient { method: options.method, proxy: false, httpAgent: agent, + // axios picks the agent by protocol, so an https:// request ignores httpAgent + httpsAgent: agent, data: options.body, + responseType: options.pipeTo ? "stream" : undefined, + onDownloadProgress: options.onDownloadProgress, }).catch((err) => { this.$logger.trace("An error occurred while sending the request:", err); if (err.response) { @@ -137,9 +142,18 @@ export class HttpClient implements Server.IHttpClient { if (result) { this.$logger.trace( "httpRequest: Done. code = %d", - result.status.toString() + result.status.toString(), ); + if (options.pipeTo) { + await pipeline(result.data, options.pipeTo); + + return { + response: result, + headers: result.headers, + }; + } + return { response: result, body: JSON.stringify(result.data), @@ -152,7 +166,7 @@ export class HttpClient implements Server.IHttpClient { if (statusCode === HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED) { const clientNameLowerCase = this.$staticConfig.CLIENT_NAME.toLowerCase(); this.$logger.error( - `You can run ${EOL}\t${clientNameLowerCase} proxy set .${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.` + `You can run ${EOL}\t${clientNameLowerCase} proxy set .${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.`, ); return "Your proxy requires authentication."; } else if (statusCode === HttpStatusCodes.PAYMENT_REQUIRED) { @@ -177,7 +191,7 @@ export class HttpClient implements Server.IHttpClient { } catch (parsingFailed) { this.$logger.trace( "Failed to get error from http request: ", - parsingFailed + parsingFailed, ); return `The server returned unexpected response: ${body}`; } @@ -199,7 +213,7 @@ export class HttpClient implements Server.IHttpClient { cliProxySettings: IProxySettings, options: any, headers: any, - requestProto: string + requestProto: string, ): Promise { const isLocalRequest = options.host === "localhost" || options.host === "127.0.0.1"; diff --git a/lib/constants.ts b/lib/constants.ts index e6584b274f..1c50e69870 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -217,6 +217,15 @@ export const DEBUGGER_ATTACHED_EVENT_NAME = "debuggerAttached"; export const DEBUGGER_DETACHED_EVENT_NAME = "debuggerDetached"; export const VERSION_STRING = "version"; export const INSPECTOR_CACHE_DIRNAME = "ios-inspector"; +export const BUNDLETOOL_CACHE_DIRNAME = "bundletool"; +export const BUNDLETOOL_VERSION = "1.18.2"; +// sha256 of bundletool-all-.jar as published on GitHub; +// must be updated together with BUNDLETOOL_VERSION or the download is rejected +export const BUNDLETOOL_SHA256 = + "378b5434cd1378bef6b2bc527b8c7f0ff2584b273830335bce54d6d0813c8584"; +export const BUNDLETOOL_RELEASES_URL = + "https://github.com/google/bundletool/releases/download"; +export const BUNDLETOOL_PATH_ENV_VAR = "NS_BUNDLETOOL_PATH"; export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; const ANDROID_SIGNING_REQUIRED_MESSAGE = "you need to specify all --key-store-* options."; diff --git a/lib/services/android/android-bundle-tool-service.ts b/lib/services/android/android-bundle-tool-service.ts index 32dba7d8e1..7afe726bf3 100644 --- a/lib/services/android/android-bundle-tool-service.ts +++ b/lib/services/android/android-bundle-tool-service.ts @@ -1,31 +1,61 @@ -import { resolve, join } from "path"; +import { join } from "path"; import * as _ from "lodash"; import { hasValidAndroidSigning } from "../../common/helpers"; -import { IChildProcess, ISysInfo, IErrors } from "../../common/declarations"; +import { + IChildProcess, + IErrors, + IFileSystem, + ISettingsService, + ISysInfo, + Server, +} from "../../common/declarations"; import { IAndroidBundleToolService, IBuildApksOptions, IInstallApksOptions, } from "../../definitions/android-bundle-tool-service"; +import { ITerminalSpinnerService } from "../../definitions/terminal-spinner-service"; +import { + BUNDLETOOL_CACHE_DIRNAME, + BUNDLETOOL_PATH_ENV_VAR, + BUNDLETOOL_RELEASES_URL, + BUNDLETOOL_SHA256, + BUNDLETOOL_VERSION, +} from "../../constants"; import { injector } from "../../common/yok"; export class AndroidBundleToolService implements IAndroidBundleToolService { + // a cold download of ~32MB can outlast the default 10s lock, and a second + // process has to keep waiting rather than pull the same jar again + private static LOCK_OPTIONS: ILockOptions = { + stale: 5 * 60 * 1000, + retriesObj: { + retries: 300, + minTimeout: 200, + maxTimeout: 2000, + factor: 1.5, + }, + }; + private javaPath: string; - private aabToolPath: string; + private bundleToolPathPromise: Promise; + constructor( private $childProcess: IChildProcess, private $sysInfo: ISysInfo, - private $errors: IErrors - ) { - this.aabToolPath = resolve( - join(__dirname, "../../../vendor/aab-tool/bundletool.jar") - ); - } + private $errors: IErrors, + private $fs: IFileSystem, + private $httpClient: Server.IHttpClient, + private $lockService: ILockService, + private $logger: ILogger, + private $settingsService: ISettingsService, + private $terminalSpinnerService: ITerminalSpinnerService, + ) {} public async buildApks(options: IBuildApksOptions): Promise { if (!hasValidAndroidSigning(options.signingData)) { this.$errors.fail( - `Unable to build "apks" without a full signing information.` + `Unable to build "apks" without a full signing information.`, ); } @@ -46,7 +76,7 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { ]); if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) { this.$errors.fail( - `Unable to build "apks" from the provided "aab". Error: ${aabToolResult.stderr}` + `Unable to build "apks" from the provided "aab". Error: ${aabToolResult.stderr}`, ); } } @@ -61,18 +91,18 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { ]); if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) { this.$errors.fail( - `Unable to install "apks" on device "${options.deviceId}". Error: ${aabToolResult.stderr}` + `Unable to install "apks" on device "${options.deviceId}". Error: ${aabToolResult.stderr}`, ); } } private async execBundleTool(args: string[]) { const javaPath = await this.getJavaPath(); - const defaultArgs = ["-jar", this.aabToolPath]; + const defaultArgs = ["-jar", await this.getBundleToolPath()]; const result = await this.$childProcess.trySpawnFromCloseEvent( javaPath, - _.concat(defaultArgs, args) + _.concat(defaultArgs, args), ); return result; @@ -85,6 +115,130 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { return this.javaPath; } + + private getBundleToolPath(): Promise { + this.bundleToolPathPromise = + this.bundleToolPathPromise || this.resolveBundleToolPath(); + + return this.bundleToolPathPromise; + } + + private async resolveBundleToolPath(): Promise { + const customPath = process.env[BUNDLETOOL_PATH_ENV_VAR]; + if (customPath) { + if (!this.$fs.exists(customPath)) { + this.$errors.fail( + `${BUNDLETOOL_PATH_ENV_VAR} is set to "${customPath}", but no file exists there.`, + ); + } + + this.$logger.trace( + `Using bundletool from ${BUNDLETOOL_PATH_ENV_VAR}: ${customPath}`, + ); + return customPath; + } + + const cacheDir = join( + this.$settingsService.getProfileDir(), + BUNDLETOOL_CACHE_DIRNAME, + ); + const jarPath = join(cacheDir, `bundletool-all-${BUNDLETOOL_VERSION}.jar`); + this.$fs.ensureDirectoryExists(cacheDir); + + if (await this.isExpectedJar(jarPath)) { + return jarPath; + } + + return this.$lockService.executeActionWithLock( + async () => { + // whoever held the lock before us may have just downloaded it + if (await this.isExpectedJar(jarPath)) { + return jarPath; + } + + await this.downloadBundleTool(jarPath); + return jarPath; + }, + join(cacheDir, `bundletool-${BUNDLETOOL_VERSION}.lock`), + AndroidBundleToolService.LOCK_OPTIONS, + ); + } + + private async isExpectedJar(jarPath: string): Promise { + if (!this.$fs.exists(jarPath)) { + return false; + } + + const shasum = await this.$fs.getFileShasum(jarPath, { + algorithm: "sha256", + }); + if (shasum === BUNDLETOOL_SHA256) { + return true; + } + + this.$logger.warn( + `Cached bundletool at "${jarPath}" does not match the expected checksum and will be downloaded again.`, + ); + this.$fs.deleteFile(jarPath); + return false; + } + + private async downloadBundleTool(jarPath: string): Promise { + const jarName = `bundletool-all-${BUNDLETOOL_VERSION}.jar`; + const url = `${BUNDLETOOL_RELEASES_URL}/${BUNDLETOOL_VERSION}/${jarName}`; + const tempPath = `${jarPath}.download`; + const spinner = this.$terminalSpinnerService.createSpinner(); + + spinner.start(`Downloading bundletool ${BUNDLETOOL_VERSION}`); + + try { + await this.$httpClient.httpRequest({ + url, + method: "GET", + // identity keeps Content-Length equal to the real jar size, so the + // progress readout is not skewed by a re-compressed response + headers: { "Accept-Encoding": "identity" }, + pipeTo: this.$fs.createWriteStream(tempPath), + onDownloadProgress: (progress: { loaded: number; total?: number }) => { + spinner.text = `Downloading bundletool ${BUNDLETOOL_VERSION} ${this.formatProgress( + progress, + )}`; + }, + }); + } catch (err) { + spinner.fail(`Failed to download bundletool ${BUNDLETOOL_VERSION}`); + this.$fs.deleteFile(tempPath); + this.$errors.fail( + `Unable to download bundletool from "${url}". ` + + `Set ${BUNDLETOOL_PATH_ENV_VAR} to the path of a local bundletool jar to skip the download. ` + + `Error: ${err.message}`, + ); + } + + const shasum = await this.$fs.getFileShasum(tempPath, { + algorithm: "sha256", + }); + if (shasum !== BUNDLETOOL_SHA256) { + spinner.fail(`Failed to download bundletool ${BUNDLETOOL_VERSION}`); + this.$fs.deleteFile(tempPath); + this.$errors.fail( + `Checksum mismatch for bundletool downloaded from "${url}". ` + + `Expected ${BUNDLETOOL_SHA256}, got ${shasum}.`, + ); + } + + // rename is atomic, so a concurrent reader never sees a partial jar + this.$fs.rename(tempPath, jarPath); + spinner.succeed(`Downloaded bundletool ${BUNDLETOOL_VERSION}`); + } + + private formatProgress(progress: { loaded: number; total?: number }): string { + const toMb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1); + + return progress.total + ? `${toMb(progress.loaded)}/${toMb(progress.total)} MB` + : `${toMb(progress.loaded)} MB`; + } } injector.register("androidBundleToolService", AndroidBundleToolService); diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js index 6c57e5f0f6..b9d7ae0ac2 100644 --- a/scripts/copy-assets.js +++ b/scripts/copy-assets.js @@ -48,7 +48,6 @@ function copyFile(sourcePath, targetPath) { const source = fs.statSync(sourcePath); if (fs.existsSync(targetPath)) { const target = fs.statSync(targetPath); - // vendor/ alone is ~31MB; re-copying it on every build is pure waste if (target.mtimeMs >= source.mtimeMs && target.size === source.size) { skipped++; return; diff --git a/test/services/android-bundle-tool-service.ts b/test/services/android-bundle-tool-service.ts new file mode 100644 index 0000000000..370eb28719 --- /dev/null +++ b/test/services/android-bundle-tool-service.ts @@ -0,0 +1,239 @@ +import { join } from "path"; +import { assert } from "chai"; +import { Yok } from "../../lib/common/yok"; +import { IInjector } from "../../lib/common/definitions/yok"; +import { AndroidBundleToolService } from "../../lib/services/android/android-bundle-tool-service"; +import { IAndroidBundleToolService } from "../../lib/definitions/android-bundle-tool-service"; +import { + ErrorsStub, + FileSystemStub, + LoggerStub, + TerminalSpinnerServiceStub, +} from "../stubs"; +import { + BUNDLETOOL_PATH_ENV_VAR, + BUNDLETOOL_SHA256, + BUNDLETOOL_VERSION, +} from "../../lib/constants"; + +describe("androidBundleToolService", () => { + const profileDir = join("/", "profile"); + const cacheDir = join(profileDir, "bundletool"); + const jarPath = join(cacheDir, `bundletool-all-${BUNDLETOOL_VERSION}.jar`); + const tempPath = `${jarPath}.download`; + const deviceId = "emulator-5554"; + + let originalEnvValue: string; + + beforeEach(() => { + originalEnvValue = process.env[BUNDLETOOL_PATH_ENV_VAR]; + delete process.env[BUNDLETOOL_PATH_ENV_VAR]; + }); + + afterEach(() => { + if (originalEnvValue === undefined) { + delete process.env[BUNDLETOOL_PATH_ENV_VAR]; + } else { + process.env[BUNDLETOOL_PATH_ENV_VAR] = originalEnvValue; + } + }); + + const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + + testInjector.register("childProcess", { + spawnedArgs: null, + trySpawnFromCloseEvent(command: string, args: string[]) { + this.spawnedArgs = args; + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }, + }); + testInjector.register("sysInfo", { + getJavaPath: async () => "java", + }); + testInjector.register("errors", ErrorsStub); + testInjector.register("fs", FileSystemStub); + testInjector.register("httpClient", { + requests: [], + async httpRequest(options: any) { + this.requests.push(options); + return {}; + }, + }); + testInjector.register("lockService", { + lockedActions: 0, + executeActionWithLock(action: () => Promise): Promise { + this.lockedActions++; + return action(); + }, + }); + testInjector.register("logger", LoggerStub); + testInjector.register("settingsService", { + getProfileDir: () => profileDir, + }); + testInjector.register("terminalSpinnerService", TerminalSpinnerServiceStub); + testInjector.register("androidBundleToolService", AndroidBundleToolService); + + return testInjector; + }; + + // the jar path is resolved lazily on the first bundletool invocation, so the + // arguments handed to java are what the resolution actually produced + const getJarPassedToJava = (testInjector: IInjector): string => { + const childProcess = testInjector.resolve("childProcess"); + const jarFlagIndex = childProcess.spawnedArgs.indexOf("-jar"); + + return childProcess.spawnedArgs[jarFlagIndex + 1]; + }; + + const installApks = (testInjector: IInjector): Promise => { + const service = testInjector.resolve( + "androidBundleToolService", + ); + + return service.installApks({ apksFilePath: "my.apks", deviceId }); + }; + + describe("resolving bundletool", () => { + it("uses the jar pointed at by the env var without downloading", async () => { + const customPath = join("/", "opt", "bundletool.jar"); + process.env[BUNDLETOOL_PATH_ENV_VAR] = customPath; + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + + await installApks(testInjector); + + assert.equal(getJarPassedToJava(testInjector), customPath); + assert.lengthOf(httpClient.requests, 0); + }); + + it("fails when the env var points at a missing file", async () => { + const customPath = join("/", "opt", "missing.jar"); + process.env[BUNDLETOOL_PATH_ENV_VAR] = customPath; + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = () => false; + + await assert.isRejected( + installApks(testInjector), + `${BUNDLETOOL_PATH_ENV_VAR} is set to "${customPath}", but no file exists there.`, + ); + }); + + it("reuses the cached jar when its checksum matches", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + fs.exists = (path: string) => path === jarPath; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + + await installApks(testInjector); + + assert.equal(getJarPassedToJava(testInjector), jarPath); + assert.lengthOf(httpClient.requests, 0); + }); + + it("resolves the jar only once across multiple invocations", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + let shasumCalls = 0; + fs.exists = (path: string) => path === jarPath; + fs.getFileShasum = async () => { + shasumCalls++; + return BUNDLETOOL_SHA256; + }; + + await installApks(testInjector); + await installApks(testInjector); + + assert.equal(shasumCalls, 1); + }); + }); + + describe("downloading bundletool", () => { + it("downloads, verifies and atomically moves the jar into the cache", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + const renames: { from: string; to: string }[] = []; + fs.exists = () => false; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + fs.rename = (from: string, to: string) => { + renames.push({ from, to }); + }; + + await installApks(testInjector); + + assert.lengthOf(httpClient.requests, 1); + assert.equal( + httpClient.requests[0].url, + `https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar`, + ); + assert.deepEqual(renames, [{ from: tempPath, to: jarPath }]); + assert.equal(getJarPassedToJava(testInjector), jarPath); + }); + + it("takes the lock and re-checks the cache before downloading", async () => { + const testInjector = createTestInjector(); + const lockService = testInjector.resolve("lockService"); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + // absent when first checked, present by the time the lock is acquired, + // as if another process downloaded it while this one waited + let existsCalls = 0; + fs.exists = () => existsCalls++ > 0; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + + await installApks(testInjector); + + assert.equal(lockService.lockedActions, 1); + assert.lengthOf(httpClient.requests, 0); + assert.equal(getJarPassedToJava(testInjector), jarPath); + }); + + it("deletes the partial download and fails on a checksum mismatch", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = () => false; + fs.getFileShasum = async () => "deadbeef"; + + await assert.isRejected( + installApks(testInjector), + /Checksum mismatch for bundletool/, + ); + assert.include(fs.deletedFiles, tempPath); + }); + + it("deletes the partial download and points at the env var when the download fails", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const httpClient = testInjector.resolve("httpClient"); + fs.exists = () => false; + httpClient.httpRequest = async () => { + throw new Error("socket hang up"); + }; + + await assert.isRejected( + installApks(testInjector), + new RegExp( + `Unable to download bundletool.*${BUNDLETOOL_PATH_ENV_VAR}.*socket hang up`, + ), + ); + assert.include(fs.deletedFiles, tempPath); + }); + + it("discards a cached jar whose checksum no longer matches", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + const shasums = ["tampered", "tampered", BUNDLETOOL_SHA256]; + fs.exists = () => shasums.length > 1; + fs.getFileShasum = async () => shasums.shift(); + + await installApks(testInjector); + + assert.include(fs.deletedFiles, jarPath); + assert.lengthOf(httpClient.requests, 1); + }); + }); +}); diff --git a/vendor/aab-tool/LICENSE b/vendor/aab-tool/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/vendor/aab-tool/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/vendor/aab-tool/README.txt b/vendor/aab-tool/README.txt deleted file mode 100644 index c57c515602..0000000000 --- a/vendor/aab-tool/README.txt +++ /dev/null @@ -1 +0,0 @@ -Downloaded from https://github.com/google/bundletool/releases/tag/1.18.2 \ No newline at end of file diff --git a/vendor/aab-tool/bundletool.jar b/vendor/aab-tool/bundletool.jar deleted file mode 100644 index b95c3beb9b..0000000000 Binary files a/vendor/aab-tool/bundletool.jar and /dev/null differ