From 96669880398d3df3fc8cfe12c82f3e94fa7ef36e Mon Sep 17 00:00:00 2001 From: Iurii Makhno Date: Wed, 9 Mar 2022 10:39:56 +0000 Subject: [PATCH] Prepare for release 1.9.0. --- README.md | 2 +- archive/com/google/android/archive/README.md | 2 + .../android/archive/ReactivateActivity.java | 119 +++++ .../archive/UpdateBroadcastReceiver.java | 43 ++ build.gradle | 141 ++--- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../bundletool/androidtools/P7ZipCommand.java | 58 +++ .../ArchivedAndroidManifestUtils.java} | 29 +- .../ArchivedApksGenerator.java} | 80 +-- .../bundletool/commands/AppBundleModule.java | 7 +- .../bundletool/commands/BuildApksCommand.java | 144 +++++- .../bundletool/commands/BuildApksManager.java | 86 ++-- .../commands/BuildApksManagerComponent.java | 2 +- .../bundletool/commands/BuildApksModule.java | 1 - .../commands/BuildSdkApksCommand.java | 451 ++++++++++++++++ .../commands/BuildSdkApksManager.java | 93 ++++ .../BuildSdkApksManagerComponent.java | 51 ++ .../commands/BuildSdkApksModule.java | 117 +++++ .../bundletool/commands/CommandHelp.java | 2 +- .../tools/build/bundletool/flags/Flag.java | 5 + .../bundletool/io/Aapt2ResourceConverter.java | 209 ++++++++ .../bundletool/io/ApkDescriptionHelper.java | 92 ++++ .../build/bundletool/io/ApkPathManager.java | 16 +- .../build/bundletool/io/ApkSerializer.java | 67 +++ .../bundletool/io/ApkSerializerHelper.java | 25 +- .../bundletool/io/ApkSerializerManager.java | 257 ++++------ .../bundletool/io/ApkSerializerModule.java | 16 +- .../bundletool/io/ApkSetBuilderFactory.java | 261 ---------- .../build/bundletool/io/ApkSetWriter.java | 121 +++++ .../io/ApkzlibApkSerializerHelper.java | 277 ---------- .../bundletool/io/AppBundleSerializer.java | 8 + .../bundletool/io/ModuleEntriesPack.java | 165 ++++++ .../bundletool/io/ModuleEntriesPacker.java | 104 ++++ .../bundletool/io/ModuleSplitSerializer.java | 372 ++++++++++++++ .../bundletool/io/SdkBundleSerializer.java | 84 +++ .../io/SerializationFilesManager.java | 86 ++++ .../bundletool/io/SplitApkSerializer.java | 82 --- .../io/StandaloneApkSerializer.java | 105 ---- .../build/bundletool/io/ZipEntrySource.java | 3 +- ...lper.java => ZipFlingerApkSerializer.java} | 37 +- .../tools/build/bundletool/io/ZipReader.java | 4 +- .../tools/build/bundletool/io/Zipper.java | 97 ++++ .../bundletool/mergers/MergingUtils.java | 10 +- .../mergers/ModuleSplitsToShardMerger.java | 21 +- .../mergers/ResourceTableMerger.java | 2 +- .../bundletool/model/AndroidManifest.java | 63 ++- .../build/bundletool/model/ApkModifier.java | 2 +- .../build/bundletool/model/AppBundle.java | 158 +----- .../tools/build/bundletool/model/Bundle.java | 30 ++ .../build/bundletool/model/BundleModule.java | 14 +- .../model/ClassesDexNameSanitizer.java | 2 +- .../build/bundletool/model/GeneratedApks.java | 14 +- .../bundletool/model/ManifestEditor.java | 23 +- .../bundletool/model/ModuleAbiSanitizer.java | 2 +- .../build/bundletool/model/ModuleEntry.java | 14 +- .../build/bundletool/model/ModuleSplit.java | 77 ++- .../build/bundletool/model/SdkBundle.java | 132 +++++ .../build/bundletool/model/VariantKey.java | 4 +- .../tools/build/bundletool/model/ZipPath.java | 2 +- .../exceptions/InternalExceptionBuilder.java | 2 +- .../exceptions/UserExceptionBuilder.java | 2 +- .../AlternativeVariantTargetingPopulator.java | 4 +- .../targeting/ScreenDensitySelector.java | 2 +- .../model/targeting/TargetedDirectory.java | 26 + .../targeting/TargetedDirectorySegment.java | 2 +- .../model/targeting/TargetingUtils.java | 15 +- .../bundletool/model/utils/BundleParser.java | 185 +++++++ .../model/utils/LocaleConfigXmlInjector.java | 202 ++++++++ .../model/utils/ResourcesUtils.java | 7 + .../bundletool/model/utils/ResultUtils.java | 8 +- .../model/utils/SplitsXmlInjector.java | 53 +- .../model/version/BundleToolVersion.java | 2 +- .../model/version/VersionGuardedFeature.java | 9 +- .../EmbeddedApkSigningPreprocessor.java | 2 +- .../EntryCompressionPreprocessor.java | 2 +- .../shards/StandaloneApksGenerator.java | 3 +- .../validation/AppBundleValidator.java | 4 +- .../validation/BundleModulesValidator.java | 3 +- .../validation/BundleZipValidator.java | 2 +- .../validation/DeviceTierParityValidator.java | 46 ++ .../MandatoryFilesPresenceValidator.java | 9 +- .../SdkAndroidManifestValidator.java | 170 ++++++ .../SdkBundleHasOneModuleValidator.java | 57 +++ .../SdkBundleModuleNameValidator.java | 35 ++ .../validation/SdkBundleValidator.java | 85 +++ .../bundletool/validation/SubValidator.java | 9 +- .../validation/ValidatorRunner.java | 18 + src/main/proto/app_integrity_config.proto | 4 +- src/main/proto/commands.proto | 37 +- src/main/proto/config.proto | 17 + src/main/proto/rotation_config.proto | 7 +- .../proto/runtime_enabled_sdk_config.proto | 23 + .../build/bundletool/archive/dex/classes.dex | Bin 0 -> 6460 bytes .../build/bundletool/internal/dex/classes.dex | Bin 6628 -> 0 bytes .../commands/BuildApksCommandTest.java | 279 +++++++--- .../BuildApksManagerOldSerializerTest.java | 30 ++ .../commands/BuildApksManagerTest.java | 446 +++++++++++----- .../BuildApksResourcePinningTest.java | 3 +- .../commands/BuildSdkApksCommandTest.java | 483 ++++++++++++++++++ .../commands/BuildSdkApksManagerTest.java | 292 +++++++++++ .../CheckTransparencyCommandTest.java | 22 +- .../ModuleSplitsToShardMergerTest.java | 8 +- .../mergers/SameTargetingMergerTest.java | 3 +- .../bundletool/model/AndroidManifestTest.java | 101 ++++ .../bundletool/model/BundleModuleTest.java | 43 ++ .../bundletool/model/GeneratedApksTest.java | 16 +- .../bundletool/model/ManifestEditorTest.java | 93 ++++ .../bundletool/model/ModuleEntryTest.java | 38 ++ .../bundletool/model/ModuleSplitTest.java | 68 ++- .../build/bundletool/model/SdkBundleTest.java | 102 ++++ ...ernativeVariantTargetingPopulatorTest.java | 13 +- .../utils/LocaleConfigXmlInjectorTest.java | 338 ++++++++++++ .../model/utils/ResultUtilsTest.java | 29 +- .../model/utils/SplitsXmlInjectorTest.java | 102 ++-- .../build/bundletool/testing/ApkSetUtils.java | 9 + .../testing/ApksArchiveHelpers.java | 8 +- .../testing/BundleModuleBuilder.java | 7 + .../testing/ManifestProtoUtils.java | 39 ++ .../bundletool/testing/SdkBundleBuilder.java | 81 +++ .../build/bundletool/testing/TestModule.java | 84 ++- .../build/bundletool/testing/TestUtils.java | 91 ++++ .../bundletool/testing/ZipAlignCheck.java | 38 +- .../DeviceTierParityValidatorTest.java | 89 +++- .../MandatoryFilesPresenceValidatorTest.java | 19 +- .../SdkAndroidManifestValidatorTest.java | 305 +++++++++++ .../SdkBundleHasOneModuleValidatorTest.java | 66 +++ .../SdkBundleModuleNameValidatorTest.java | 65 +++ .../validation/ValidatorRunnerTest.java | 23 + .../bundletool/validation/ValidatorsTest.java | 2 + 130 files changed, 7257 insertions(+), 1725 deletions(-) create mode 100644 archive/com/google/android/archive/README.md create mode 100644 archive/com/google/android/archive/ReactivateActivity.java create mode 100644 archive/com/google/android/archive/UpdateBroadcastReceiver.java create mode 100644 src/main/java/com/android/tools/build/bundletool/androidtools/P7ZipCommand.java rename src/main/java/com/android/tools/build/bundletool/{internal/HibernatedAndroidManifestUtils.java => archive/ArchivedAndroidManifestUtils.java} (85%) rename src/main/java/com/android/tools/build/bundletool/{internal/HibernatedApksGenerator.java => archive/ArchivedApksGenerator.java} (50%) create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManager.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerComponent.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksModule.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/Aapt2ResourceConverter.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ApkSerializer.java delete mode 100644 src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java delete mode 100644 src/main/java/com/android/tools/build/bundletool/io/ApkzlibApkSerializerHelper.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPack.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/SdkBundleSerializer.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/SerializationFilesManager.java delete mode 100644 src/main/java/com/android/tools/build/bundletool/io/SplitApkSerializer.java delete mode 100644 src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java rename src/main/java/com/android/tools/build/bundletool/io/{ZipFlingerApkSerializerHelper.java => ZipFlingerApkSerializer.java} (90%) create mode 100644 src/main/java/com/android/tools/build/bundletool/io/Zipper.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/Bundle.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java create mode 100644 src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/validation/SdkBundleValidator.java create mode 100644 src/main/proto/runtime_enabled_sdk_config.proto create mode 100644 src/main/resources/com/android/tools/build/bundletool/archive/dex/classes.dex delete mode 100644 src/main/resources/com/android/tools/build/bundletool/internal/dex/classes.dex create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerOldSerializerTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjectorTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java create mode 100644 src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidatorTest.java diff --git a/README.md b/README.md index 6d7fab91..f9ad49ff 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.8.2](https://github.com/google/bundletool/releases) +Latest release: [1.9.0](https://github.com/google/bundletool/releases) diff --git a/archive/com/google/android/archive/README.md b/archive/com/google/android/archive/README.md new file mode 100644 index 00000000..dddf5703 --- /dev/null +++ b/archive/com/google/android/archive/README.md @@ -0,0 +1,2 @@ +Source-code for java/com/android/tools/build/bundletool/archive/dex/classes.dex +which is used to build APKs with build-mode ARCHIVE. diff --git a/archive/com/google/android/archive/ReactivateActivity.java b/archive/com/google/android/archive/ReactivateActivity.java new file mode 100644 index 00000000..3b76a003 --- /dev/null +++ b/archive/com/google/android/archive/ReactivateActivity.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * 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 + */ + +package com.google.android.archive; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; + +/** Activity that triggers the reactivation of an app through the app store. */ +public class ReactivateActivity extends Activity implements DialogInterface.OnClickListener { + + public static final String STORE_PACKAGE_NAME = "com.android.vending"; + + private boolean processingError; + + @Override + public void onResume() { + super.onResume(); + // Ensures we don't try to send another intent immediately after the first one failed, instead + // an error dialog is shown. + if (processingError) { + return; + } + Intent intent = new Intent(); + intent.setAction("com.google.android.STORE_ARCHIVE"); + intent.setPackage(STORE_PACKAGE_NAME); + try { + startActivityForResult(intent, /* flags= */ 0); + } catch (ActivityNotFoundException e) { + // We handle this case in onActivityResult, because a RESULT_CANCELED is emitted. + } + } + + /** Returns true if the targeted Store is installed and enabled. */ + private boolean isStoreInstalled() { + try { + return getPackageManager().getApplicationInfo(STORE_PACKAGE_NAME, 0).enabled; + } catch (NameNotFoundException e) { + return false; + } + } + + private AlertDialog buildErrorDialog() { + AlertDialog.Builder dialog = + new AlertDialog.Builder(this) + .setTitle("Installation failed") + .setCancelable(false) + .setNeutralButton("Close", this) + .setMessage( + String.format( + "The app %s is currently archived and must be reinstalled from an" + + " official app store.", + getAppName())); + + if (isStoreInstalled()) { + dialog.setPositiveButton("Reinstall", this); + } + + return dialog.create(); + } + + private String getAppName() { + return getApplicationInfo().loadLabel(getPackageManager()).toString(); + } + + @Override + public void onClick(DialogInterface ignored, int buttonType) { + processingError = false; + switch (buttonType) { + case DialogInterface.BUTTON_POSITIVE: + openStorePageForApp(); + break; + case DialogInterface.BUTTON_NEUTRAL: + default: + // Nothing specific, just close the app. + finish(); + break; + } + } + + private void openStorePageForApp() { + Intent intent = + new Intent(Intent.ACTION_VIEW) + .setPackage(STORE_PACKAGE_NAME) + .setData(Uri.parse(String.format("market://details?id=%s", getPackageName()))); + + startActivity(intent); + } + + @Override + public void onActivityResult(int ignored1, int resultCode, Intent ignored2) { + if (resultCode == Activity.RESULT_CANCELED) { + processingError = true; + buildErrorDialog().show(); + } else { + // At this point the reactivation has completed and the app should be restarted immediately, + // if there is some delay here we don't want to show an empty activity. + finish(); + } + } +} diff --git a/archive/com/google/android/archive/UpdateBroadcastReceiver.java b/archive/com/google/android/archive/UpdateBroadcastReceiver.java new file mode 100644 index 00000000..387ac038 --- /dev/null +++ b/archive/com/google/android/archive/UpdateBroadcastReceiver.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * 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 + */ + +package com.google.android.archive; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import java.io.File; + +/** Clears up the app's cache once it has been archived. */ +public class UpdateBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { + deleteDir(context.getCacheDir()); + } + } + + private static void deleteDir(File dir) { + File[] contents = dir.listFiles(); + if (contents != null) { + for (File file : contents) { + deleteDir(file); + } + } + dir.delete(); + } +} diff --git a/build.gradle b/build.gradle index fa853dbb..7553c933 100644 --- a/build.gradle +++ b/build.gradle @@ -8,15 +8,15 @@ buildscript { } } dependencies { - classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.8" - classpath "com.github.jengelman.gradle.plugins:shadow:4.0.4" + classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.13" + classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.1" } } apply plugin: "com.github.johnrengelman.shadow" apply plugin: "com.google.protobuf" apply plugin: "java" -apply plugin: "maven" +apply plugin: "maven-publish" repositories { mavenCentral() @@ -24,19 +24,18 @@ repositories { } configurations { - compileWindows - compileMacOs - compileLinux + implementationWindows + implementationMacOs + implementationLinux } // The repackaging rules are defined in the "shadowJar" task below. dependencies { - compile "com.android.tools:common:30.1.0-alpha07" - compile "com.android.tools:r8:2.2.64" - compile "com.android.tools.build:apkzlib:4.2.0-alpha13" - compile "com.android.tools.build:apksig:4.2.0-alpha13" - compile "com.android.tools.ddms:ddmlib:30.1.0-alpha07" - compile "com.android:zipflinger:7.1.0-alpha07" + implementation "com.android.tools:common:30.1.0-alpha07" + implementation "com.android.tools:r8:2.2.64" + implementation "com.android.tools.build:apksig:4.2.0-alpha13" + implementation "com.android.tools.ddms:ddmlib:30.1.0-alpha07" + implementation "com.android:zipflinger:7.1.0-alpha07" shadow "com.android.tools.build:aapt2-proto:7.0.0-beta04-7396180" shadow "com.google.auto.value:auto-value-annotations:1.6.2" @@ -53,39 +52,43 @@ dependencies { } shadow "org.slf4j:slf4j-api:1.7.30" - compileWindows "com.android.tools.build:aapt2:7.0.0-beta04-7396180:windows" - compileMacOs "com.android.tools.build:aapt2:7.0.0-beta04-7396180:osx" - compileLinux "com.android.tools.build:aapt2:7.0.0-beta04-7396180:linux" + implementationWindows "com.android.tools.build:aapt2:7.0.0-beta04-7396180:windows" + implementationMacOs "com.android.tools.build:aapt2:7.0.0-beta04-7396180:osx" + implementationLinux "com.android.tools.build:aapt2:7.0.0-beta04-7396180:linux" - runtime "org.slf4j:slf4j-jdk14:1.7.30" + compileOnly "org.bouncycastle:bcprov-jdk15on:1.56" + compileOnly "org.bouncycastle:bcpkix-jdk15on:1.56" + runtimeOnly "org.slf4j:slf4j-jdk14:1.7.30" - testCompile "com.android.tools.build:aapt2-proto:7.0.0-beta04-7396180" - testCompile "com.google.auto.value:auto-value-annotations:1.6.2" + testImplementation "com.android.tools.build:aapt2-proto:7.0.0-beta04-7396180" + testImplementation "com.google.auto.value:auto-value-annotations:1.6.2" testAnnotationProcessor "com.google.auto.value:auto-value:1.6.2" - testCompile "com.google.errorprone:error_prone_annotations:2.3.1" - testCompile "com.google.guava:guava:30.1-jre" - testCompile "com.google.truth.extensions:truth-java8-extension:0.45" - testCompile "com.google.truth.extensions:truth-proto-extension:0.45" - testCompile "com.google.jimfs:jimfs:1.1" - testCompile "com.google.protobuf:protobuf-java:3.10.0" - testCompile "com.google.protobuf:protobuf-java-util:3.10.0" - testCompile "org.mockito:mockito-core:2.18.3" - testCompile "junit:junit:4.12" - testCompile "org.junit.jupiter:junit-jupiter-api:5.2.0" - testCompile "org.junit.vintage:junit-vintage-engine:5.2.0" - testRuntime "org.junit.jupiter:junit-jupiter-engine:5.2.0" - testCompile "org.junit.platform:junit-platform-runner:1.2.0" - testCompile "com.google.dagger:dagger:2.28.3" + testImplementation "com.google.errorprone:error_prone_annotations:2.3.1" + testImplementation "com.google.guava:guava:30.1-jre" + testImplementation "com.google.truth.extensions:truth-java8-extension:0.45" + testImplementation "com.google.truth.extensions:truth-proto-extension:0.45" + testImplementation "com.google.jimfs:jimfs:1.1" + testImplementation "com.google.protobuf:protobuf-java:3.10.0" + testImplementation "com.google.protobuf:protobuf-java-util:3.10.0" + testImplementation "org.mockito:mockito-core:2.18.3" + testImplementation "junit:junit:4.12" + testImplementation "org.bouncycastle:bcprov-jdk15on:1.56" + testImplementation "org.bouncycastle:bcpkix-jdk15on:1.56" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.2.0" + testImplementation "org.junit.vintage:junit-vintage-engine:5.2.0" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.2.0" + testImplementation "org.junit.platform:junit-platform-runner:1.2.0" + testImplementation "com.google.dagger:dagger:2.28.3" testAnnotationProcessor "com.google.dagger:dagger-compiler:2.28.3" - testCompile "javax.inject:javax.inject:1" - testCompile("org.smali:dexlib2:2.3.4") { + testImplementation "javax.inject:javax.inject:1" + testImplementation("org.smali:dexlib2:2.3.4") { exclude group: "com.google.guava", module: "guava" } - testCompile("org.bitbucket.b_c:jose4j:0.7.0") { + testImplementation("org.bitbucket.b_c:jose4j:0.7.0") { exclude group: "org.slf4j", module: "slf4j-api" } - testCompile "org.slf4j:slf4j-api:1.7.30" - testRuntime "org.slf4j:slf4j-jdk14:1.7.30" + testImplementation "org.slf4j:slf4j-api:1.7.30" + testRuntimeOnly "org.slf4j:slf4j-jdk14:1.7.30" } def osName = System.getProperty("os.name").toLowerCase() @@ -122,25 +125,45 @@ protobuf { } } -uploadShadow { - repositories { - mavenDeployer { - def localRepo = project.hasProperty('localRepo') ? - project.localRepo : "$buildDir/repo" - repository(url: "file://" + localRepo) - pom.project { - groupId 'com.android.tools.build' - artifactId 'bundletool' - version project.release_version +publishing { + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + groupId = 'com.android.tools.build' + artifactId = 'bundletool' + version = project.release_version + + pom { licenses { license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' } } } + + pom.withXml { xml -> + def jose4jDependencyNode = xml.asNode().dependencies.'*'.find { + it.artifactId.text() == 'jose4j' + } + if (jose4jDependencyNode != null) { + def exclusionNode = jose4jDependencyNode + .appendNode('exclusions') + .appendNode('exclusion'); + exclusionNode.appendNode('groupId', 'org.slf4j') + exclusionNode.appendNode('artifactId', 'slf4j-api') + } + } + } + } + repositories { + maven { + def localRepo = project.hasProperty('localRepo') ? + project.localRepo : "$buildDir/repo" + + url = "file://$localRepo" } } } @@ -183,11 +206,11 @@ task executableJar(type: ShadowJar) { baseName = 'bundletool' classifier = 'all' from sourceSets.main.output - from({ zipTree(project.configurations.compileWindows.singleFile) }) { into 'windows/' } - from({ zipTree(project.configurations.compileMacOs.singleFile) }) { into 'macos/' } - from({ zipTree(project.configurations.compileLinux.singleFile) }) { into 'linux/' } + from({ zipTree(project.configurations.implementationWindows.singleFile) }) { into 'windows/' } + from({ zipTree(project.configurations.implementationMacOs.singleFile) }) { into 'macos/' } + from({ zipTree(project.configurations.implementationLinux.singleFile) }) { into 'linux/' } configurations = [ - project.configurations.runtime, + project.configurations.runtimeClasspath, project.configurations.shadow ] manifest { @@ -201,19 +224,23 @@ task executableJar(type: ShadowJar) { // Unzip the aapt2 dependency jar. task unzipAapt2Jar(type: Copy) { if (osName.contains("linux")) { - from zipTree(project.configurations.compileLinux.singleFile) + from zipTree(project.configurations.implementationLinux.singleFile) into('build/resources/main/linux') } if (osName.contains("windows")) { - from zipTree(project.configurations.compileWindows.singleFile) + from zipTree(project.configurations.implementationWindows.singleFile) into('build/resources/main/windows') } if (osName.contains("mac")) { - from zipTree(project.configurations.compileMacOs.singleFile) + from zipTree(project.configurations.implementationMacOs.singleFile) into('build/resources/main/macos') } } +task uploadShadow { + dependsOn ':publishShadowPublicationToMavenRepository' +} + compileTestJava.dependsOn(unzipAapt2Jar) diff --git a/gradle.properties b/gradle.properties index 7f865fa5..6ef0578d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.8.2 +release_version = 1.9.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f1bee26..b935f73d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/src/main/java/com/android/tools/build/bundletool/androidtools/P7ZipCommand.java b/src/main/java/com/android/tools/build/bundletool/androidtools/P7ZipCommand.java new file mode 100644 index 00000000..c4aaab02 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/androidtools/P7ZipCommand.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.androidtools; + +import com.android.tools.build.bundletool.androidtools.CommandExecutor.CommandOptions; +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.nio.file.Path; +import java.time.Duration; + +/** Exposes 7zip commands used by Bundle Tool. */ +public interface P7ZipCommand { + + /** + * Compress all files inside {@code inputDirectoryPath} into ZIP file specified by {@code + * outputPath}. + */ + void compress(Path outputPath, Path inputDirectoryPath); + + /** + * Creates default implementation of 7zip command given path to 7zip executable and number of + * threads that should be used for compression. + */ + public static P7ZipCommand defaultP7ZipCommand(Path p7zipExecutable, int numThreads) { + return (outputPath, inputDirectoryPath) -> { + ImmutableList command = + ImmutableList.of( + p7zipExecutable.toString(), + "a", + "-tzip", + "-mtc=off", + String.format("-mx=%d", numThreads), + "-bso0", + "-bsp0", + "-r", + outputPath.toAbsolutePath().normalize().toString(), + String.join( + File.pathSeparator, + inputDirectoryPath.toAbsolutePath().normalize().toString(), + "*")); + new DefaultCommandExecutor() + .execute(command, CommandOptions.builder().setTimeout(Duration.ofMinutes(10)).build()); + }; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/internal/HibernatedAndroidManifestUtils.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java similarity index 85% rename from src/main/java/com/android/tools/build/bundletool/internal/HibernatedAndroidManifestUtils.java rename to src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java index 4b368fca..c0f7ca13 100644 --- a/src/main/java/com/android/tools/build/bundletool/internal/HibernatedAndroidManifestUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tools.build.bundletool.internal; +package com.android.tools.build.bundletool.archive; import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.google.common.base.Preconditions.checkNotNull; @@ -29,30 +29,29 @@ import com.android.tools.build.bundletool.model.version.BundleToolVersion; import java.util.Optional; -// TODO(b/193792534): Move under models/utils once INTERNAL comments are removed. -/** Utility methods for creation of hibernated manifest. */ -public final class HibernatedAndroidManifestUtils { - public static final String META_DATA_KEY_HIBERNATED = "com.android.vending.hibernated"; +/** Utility methods for creation of archived manifest. */ +public final class ArchivedAndroidManifestUtils { + public static final String META_DATA_KEY_ARCHIVED = "com.android.vending.archive"; public static final String REACTIVATE_ACTIVITY_NAME = - "com.google.android.hibernation.ReactivateActivity"; + "com.google.android.archive.ReactivateActivity"; public static final String HOLO_LIGHT_NO_ACTION_BAR_THEME = "@android:style/Theme.Holo.Light.NoActionBar"; public static final String MAIN_ACTION_NAME = "android.intent.action.MAIN"; public static final String LAUNCHER_CATEGORY_NAME = "android.intent.category.LAUNCHER"; public static final String UPDATE_BROADCAST_RECEIVER_NAME = - "com.google.android.hibernation.UpdateBroadcastReceiver"; + "com.google.android.archive.UpdateBroadcastReceiver"; public static final String MY_PACKAGE_REPLACED_ACTION_NAME = "android.intent.action.MY_PACKAGE_REPLACED"; - public static AndroidManifest createHibernatedManifest(AndroidManifest manifest) { + public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { checkNotNull(manifest); ManifestEditor editor = new ManifestEditor(createMinimalManifestTag(), BundleToolVersion.getCurrentVersion()) .setPackage(manifest.getPackageName()) - .addMetaDataBoolean(META_DATA_KEY_HIBERNATED, true); + .addMetaDataBoolean(META_DATA_KEY_ARCHIVED, true); manifest.getVersionCode().ifPresent(editor::setVersionCode); manifest.getVersionName().ifPresent(editor::setVersionName); @@ -73,21 +72,25 @@ public static AndroidManifest createHibernatedManifest(AndroidManifest manifest) if (manifest.hasLabelRefId()) { manifest.getLabelRefId().ifPresent(editor::setLabelAsRefId); } - getHibernatedAllowBackup(manifest).ifPresent(editor::setAllowBackup); + getArchivedAllowBackup(manifest).ifPresent(editor::setAllowBackup); manifest.getFullBackupOnly().ifPresent(editor::setFullBackupOnly); manifest.getFullBackupContent().ifPresent(editor::setFullBackupContent); manifest.getDataExtractionRules().ifPresent(editor::setDataExtractionRules); } + + editor.copyPermissions(manifest); + editor.copyPermissionGroups(manifest); + editor.addActivity(createReactivateActivity()); editor.addReceiver(createUpdateBroadcastReceiver()); return editor.save(); } - private static Optional getHibernatedAllowBackup(AndroidManifest manifest) { + private static Optional getArchivedAllowBackup(AndroidManifest manifest) { // Backup needs to be disabled if Backup Agent is provided and Full Backup Only is disabled. // Custom backup agent cannot be kept because it relies on app code that is not present in its - // hibernated variant. + // archived variant. return manifest.getAllowBackup().orElse(true) && (!manifest.hasBackupAgent() || manifest.getFullBackupOnly().orElse(false)) ? manifest.getAllowBackup() @@ -125,5 +128,5 @@ private static Receiver createUpdateBroadcastReceiver() { .build(); } - private HibernatedAndroidManifestUtils() {} + private ArchivedAndroidManifestUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/internal/HibernatedApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java similarity index 50% rename from src/main/java/com/android/tools/build/bundletool/internal/HibernatedApksGenerator.java rename to src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java index 81cda620..fe96e012 100644 --- a/src/main/java/com/android/tools/build/bundletool/internal/HibernatedApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java @@ -14,8 +14,9 @@ * limitations under the License */ -package com.android.tools.build.bundletool.internal; +package com.android.tools.build.bundletool.archive; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.ARCHIVED_APK_GENERATION; import static com.google.common.base.Preconditions.checkNotNull; import com.android.aapt.Resources.ResourceTable; @@ -28,6 +29,7 @@ import com.android.tools.build.bundletool.model.ResourceTableEntry; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; +import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.splitters.ResourceAnalyzer; import com.google.common.collect.ImmutableSet; import java.io.IOException; @@ -39,68 +41,82 @@ import javax.inject.Inject; /** - * Generates hibernated apk based on provided app bundle. Leaves only minimal manifest, only - * required resources and two custom actions to clear app cache and to wake up an app. + * Generates archived apk based on provided app bundle. Leaves only minimal manifest, only required + * resources and two custom actions to clear app cache and to wake up an app. */ -public final class HibernatedApksGenerator { - private static final String HIBERNATED_CLASSES_DEX_PATH = "dex/classes.dex"; +public final class ArchivedApksGenerator { + private static final String ARCHIVED_CLASSES_DEX_PATH = "dex/classes.dex"; private final TempDirectory globalTempDir; @Inject - HibernatedApksGenerator(TempDirectory globalTempDir) { + ArchivedApksGenerator(TempDirectory globalTempDir) { this.globalTempDir = globalTempDir; } - public ModuleSplit generateHibernatedApk(AppBundle appBundle) throws IOException { + public ModuleSplit generateArchivedApk(AppBundle appBundle) throws IOException { + validateRequest(appBundle); + + BundleModule baseModule = appBundle.getBaseModule(); + + AndroidManifest archivedManifest = + ArchivedAndroidManifestUtils.createArchivedManifest(baseModule.getAndroidManifest()); + Optional archivedResourceTable = + getArchivedResourceTable(appBundle, baseModule, archivedManifest); + Path archivedClassesDexFile = getArchivedClassesDexFile(); + + return ModuleSplit.forArchive( + baseModule, archivedManifest, archivedResourceTable, archivedClassesDexFile); + } + + private void validateRequest(AppBundle appBundle) { checkNotNull(appBundle); - if (!appBundle.storeArchiveEnabled()) { + + if (!ARCHIVED_APK_GENERATION.enabledForVersion( + BundleToolVersion.getVersionFromBundleConfig(appBundle.getBundleConfig()))) { throw InvalidCommandException.builder() .withInternalMessage( - "Hibernated APK cannot be generated when Store Archive configuration is disabled.") + String.format( + "Archived APK can only be generated for bundles built with version %s or higher.", + ARCHIVED_APK_GENERATION.getEnabledSinceVersion())) .build(); } - BundleModule baseModule = appBundle.getBaseModule(); - - AndroidManifest hibernatedManifest = - HibernatedAndroidManifestUtils.createHibernatedManifest(baseModule.getAndroidManifest()); - Optional hibernatedResourceTable = - getHibernatedResourceTable(appBundle, baseModule, hibernatedManifest); - Path hibernatedClassesDexFile = getHibernatedClassesDexFile(); - - return ModuleSplit.forHibernation( - baseModule, hibernatedManifest, hibernatedResourceTable, hibernatedClassesDexFile); + if (!appBundle.storeArchiveEnabled()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Archived APK cannot be generated when Store Archive configuration is disabled.") + .build(); + } } - private Optional getHibernatedResourceTable( - AppBundle appBundle, BundleModule bundleModule, AndroidManifest hibernatedManifest) + private Optional getArchivedResourceTable( + AppBundle appBundle, BundleModule bundleModule, AndroidManifest archivedManifest) throws IOException { if (!bundleModule.getResourceTable().isPresent()) { return Optional.empty(); } ImmutableSet referredResources = - new ResourceAnalyzer(appBundle) - .findAllAppResourcesReachableFromManifest(hibernatedManifest); - ResourceTable hibernatedResourceTable = + new ResourceAnalyzer(appBundle).findAllAppResourcesReachableFromManifest(archivedManifest); + ResourceTable archivedResourceTable = ResourcesUtils.filterResourceTable( bundleModule.getResourceTable().get(), /* removeEntryPredicate= */ entry -> !referredResources.contains(entry.getResourceId()), /* configValuesFilterFn= */ ResourceTableEntry::getEntry); - return Optional.of(hibernatedResourceTable); + return Optional.of(archivedResourceTable); } - private Path getHibernatedClassesDexFile() throws IOException { - Path hibernatedDexFilePath = Files.createTempFile(globalTempDir.getPath(), "classes", ".dex"); - try (InputStream inputStream = readHibernatedClassesDexFile()) { - Files.copy(inputStream, hibernatedDexFilePath, StandardCopyOption.REPLACE_EXISTING); + private Path getArchivedClassesDexFile() throws IOException { + Path archivedDexFilePath = Files.createTempFile(globalTempDir.getPath(), "classes", ".dex"); + try (InputStream inputStream = readArchivedClassesDexFile()) { + Files.copy(inputStream, archivedDexFilePath, StandardCopyOption.REPLACE_EXISTING); } - return hibernatedDexFilePath; + return archivedDexFilePath; } - private static InputStream readHibernatedClassesDexFile() { - return HibernatedApksGenerator.class.getResourceAsStream(HIBERNATED_CLASSES_DEX_PATH); + private static InputStream readArchivedClassesDexFile() { + return ArchivedApksGenerator.class.getResourceAsStream(ARCHIVED_CLASSES_DEX_PATH); } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/AppBundleModule.java b/src/main/java/com/android/tools/build/bundletool/commands/AppBundleModule.java index 188cc710..a793120b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/AppBundleModule.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/AppBundleModule.java @@ -18,13 +18,15 @@ import com.android.bundle.Config.BundleConfig; import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.Bundle; import com.android.tools.build.bundletool.model.BundleMetadata; +import dagger.Binds; import dagger.Module; import dagger.Provides; /** Dagger module for components that manipulate an App Bundle. */ @Module -public final class AppBundleModule { +public abstract class AppBundleModule { @CommandScoped @Provides @@ -39,4 +41,7 @@ static BundleMetadata provideBundleMetadata(AppBundle appBundle) { } private AppBundleModule() {} + + @Binds + abstract Bundle bundle(AppBundle bundle); } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index f03d0b90..66ff9bb8 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.ARCHIVE; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.DEFAULT; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; @@ -27,8 +28,10 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndExecutable; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileHasExtension; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.apksig.SigningCertificateLineage; import com.android.apksig.apk.ApkFormatException; @@ -36,6 +39,7 @@ import com.android.apksig.util.DataSources; import com.android.bundle.Devices.DeviceSpec; import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.androidtools.P7ZipCommand; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; import com.android.tools.build.bundletool.device.AdbServer; @@ -50,6 +54,7 @@ import com.android.tools.build.bundletool.model.KeystoreProperties; import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.Password; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.SignerConfig; import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.SigningConfigurationProvider; @@ -63,14 +68,15 @@ import com.android.tools.build.bundletool.preprocessors.AppBundlePreprocessorManager; import com.android.tools.build.bundletool.preprocessors.AppBundleRecompressor; import com.android.tools.build.bundletool.preprocessors.DaggerAppBundlePreprocessorComponent; -import com.android.tools.build.bundletool.splitters.DexCompressionSplitter; import com.android.tools.build.bundletool.validation.AppBundleValidator; +import com.android.tools.build.bundletool.validation.SdkBundleValidator; import com.android.tools.build.bundletool.validation.SubValidator; import com.google.auto.value.AutoValue; import com.google.common.base.Ascii; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.io.Closer; import com.google.common.io.MoreFiles; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -161,13 +167,13 @@ public enum OutputFormat { private static final Flag VERBOSE_FLAG = Flag.booleanFlag("verbose"); + private static final Flag P7ZIP_PATH_FLAG = Flag.path("7zip"); + // Signing-related flags: should match flags from apksig library. private static final Flag KEYSTORE_FLAG = Flag.path("ks"); private static final Flag KEY_ALIAS_FLAG = Flag.string("ks-key-alias"); private static final Flag KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass"); private static final Flag KEY_PASSWORD_FLAG = Flag.password("key-pass"); - private static final Flag MINIMUM_V3_ROTATION_API_VERSION_FLAG = - Flag.positiveInteger("min-v3-rotation-api-version"); // SourceStamp-related flags. private static final Flag CREATE_STAMP_FLAG = Flag.booleanFlag("create-stamp"); @@ -178,9 +184,17 @@ public enum OutputFormat { private static final Flag STAMP_SOURCE_FLAG = Flag.string("stamp-source"); // Key-rotation-related flags. + private static final Flag MINIMUM_V3_ROTATION_API_VERSION_FLAG = + Flag.positiveInteger("min-v3-rotation-api-version"); + private static final Flag ROTATION_MINIMUM_SDK_VERSION_FLAG = + Flag.positiveInteger("rotation-min-sdk-version"); private static final Flag LINEAGE_FLAG = Flag.path("lineage"); private static final Flag OLDEST_SIGNER_FLAG = Flag.path("oldest-signer"); + // Runtime-enabled-SDK-related flags. + private static final Flag> RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG = + Flag.pathSet("sdk-bundles"); + private static final String APK_SET_ARCHIVE_EXTENSION = "apks"; private static final SystemEnvironmentProvider DEFAULT_PROVIDER = @@ -189,18 +203,6 @@ public enum OutputFormat { // Number embedded at the beginning of a zip file to indicate its file format. private static final int ZIP_MAGIC = 0x04034b50; - /** - * Whether the new APK serializer is enabled. - * - *

Can be overridden using the system property "bundletool.serializer.zipflinger" set to - * "true". - */ - private static final boolean ENABLE_NEW_APK_SERIALIZER = - SystemEnvironmentProvider.DEFAULT_PROVIDER - .getProperty("bundletool.serializer.zipflinger") - .map(Boolean::parseBoolean) - .orElse(true); - public abstract Path getBundlePath(); public abstract Path getOutputFile(); @@ -266,7 +268,11 @@ ListeningExecutorService getExecutorService() { public abstract Optional getAssetModulesVersionOverride(); - public abstract boolean getEnableNewApkSerializer(); + public abstract boolean getEnableApkSerializerWithoutBundleRecompression(); + + public abstract Optional getP7ZipCommand(); + + public abstract ImmutableSet getRuntimeEnabledSdkBundlePaths(); public static Builder builder() { return new AutoValue_BuildApksCommand.Builder() @@ -280,7 +286,8 @@ public static Builder builder() { .setModules(ImmutableSet.of()) .setExtraValidators(ImmutableList.of()) .setSystemApkOptions(ImmutableSet.of()) - .setEnableNewApkSerializer(ENABLE_NEW_APK_SERIALIZER); + .setEnableApkSerializerWithoutBundleRecompression(false) + .setRuntimeEnabledSdkBundlePaths(ImmutableSet.of()); } /** Builder for the {@link BuildApksCommand}. */ @@ -480,7 +487,16 @@ public Builder setCreateApkSetArchive(boolean createApkSetArchive) { /** If present, will replace the version of the asset modules with the provided value. */ public abstract Builder setAssetModulesVersionOverride(long value); - public abstract Builder setEnableNewApkSerializer(boolean enabled); + public abstract Builder setEnableApkSerializerWithoutBundleRecompression(boolean value); + + /** Provides a wrapper around the execution of the 7zip commands. */ + public abstract Builder setP7ZipCommand(P7ZipCommand value); + + /** + * Provides paths to {@link SdkBundle}s for the runtime-enabled SDKs that the {@link AppBundle} + * depends on. Each file must have extension ".asb". + */ + public abstract Builder setRuntimeEnabledSdkBundlePaths(ImmutableSet sdkBundlePaths); abstract BuildApksCommand autoBuild(); @@ -681,6 +697,18 @@ static BuildApksCommand fromFlags( DEVICE_TIER_FLAG.getValue(flags).ifPresent(buildApksCommand::setDeviceTier); MODULES_FLAG.getValue(flags).ifPresent(buildApksCommand::setModules); VERBOSE_FLAG.getValue(flags).ifPresent(buildApksCommand::setVerbose); + P7ZIP_PATH_FLAG + .getValue(flags) + .ifPresent( + p7zipPath -> { + int numThreads = MAX_THREADS_FLAG.getValue(flags).orElse(DEFAULT_THREAD_POOL_SIZE); + buildApksCommand.setP7ZipCommand( + P7ZipCommand.defaultP7ZipCommand(p7zipPath, numThreads)); + }); + + RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG + .getValue(flags) + .ifPresent(buildApksCommand::setRuntimeEnabledSdkBundlePaths); flags.checkNoUnknownFlags(); @@ -699,12 +727,13 @@ public Path execute() { try (TempDirectory tempDir = new TempDirectory(getClass().getSimpleName())) { Path bundlePath; - // The new APK serializer relies on the compression of entries in the App Bundle. + // The old APK serializer relies on the compression of entries in the App Bundle. // Unfortunately, we don't know the compression level that was used when the bundle was built, // so we re-compress all entries with our desired compression level. // Exception is made when the device spec is specified, we only need a fraction of the // entries, so re-compressing all entries would be a waste of CPU. - boolean recompressAppBundle = getEnableNewApkSerializer() && !getDeviceSpec().isPresent(); + boolean recompressAppBundle = + !getDeviceSpec().isPresent() && !getEnableApkSerializerWithoutBundleRecompression(); if (recompressAppBundle) { bundlePath = tempDir.getPath().resolve("recompressed.aab"); new AppBundleRecompressor(getExecutorService()) @@ -714,13 +743,16 @@ public Path execute() { } try (ZipFile bundleZip = new ZipFile(bundlePath.toFile()); - ZipReader zipReader = ZipReader.createFromFile(bundlePath)) { + ZipReader zipReader = ZipReader.createFromFile(bundlePath); + Closer closer = Closer.create()) { AppBundleValidator bundleValidator = AppBundleValidator.create(getExtraValidators()); bundleValidator.validateFile(bundleZip); AppBundle appBundle = AppBundle.buildFromZip(bundleZip); bundleValidator.validate(appBundle); + validateSdkBundles(closer); + AppBundlePreprocessorManager appBundlePreprocessorManager = DaggerAppBundlePreprocessorComponent.builder() .setBuildApksCommand(this) @@ -784,6 +816,32 @@ private void validateInput() { "Property 'adbPath' is required when 'generateOnlyForConnectedDevice' is true."); checkFileExistsAndExecutable(getAdbPath().get()); } + + getRuntimeEnabledSdkBundlePaths() + .forEach( + path -> { + checkFileExistsAndReadable(path); + checkFileHasExtension("ASB file", path, ".asb"); + }); + } + + private void validateSdkBundles(Closer closer) throws IOException { + SdkBundleValidator sdkBundleValidator = SdkBundleValidator.create(); + ImmutableSet.Builder sdkBundleZipsBuilder = ImmutableSet.builder(); + for (Path sdkBundlePath : getRuntimeEnabledSdkBundlePaths()) { + sdkBundleZipsBuilder.add(closer.register(new ZipFile(sdkBundlePath.toFile()))); + } + + ImmutableSet sdkBundleZips = sdkBundleZipsBuilder.build(); + sdkBundleZips.forEach(sdkBundleValidator::validateFile); + + ImmutableSet sdkBundles = + sdkBundleZips.stream() + // SdkBundle#getVersionCode is not used in `build-apks`. It does not matter what + // value we set here, so we are just setting 0. + .map(sdkBundleZip -> SdkBundle.buildFromZip(sdkBundleZip, /* versionCode= */ 0)) + .collect(toImmutableSet()); + sdkBundles.forEach(sdkBundleValidator::validate); } /** @@ -853,15 +911,17 @@ public static CommandHelp help() { .setExampleValue(joinFlagOptions(ApkBuildMode.values())) .setOptional(true) .setDescription( - "Specifies which mode to run '%s' command against. Acceptable values are '%s'. " - + "If not set or set to '%s' we generate split, standalone and instant " - + "APKs. If set to '%s' we generate universal APK. If set to '%s' we " - + "generate APKs for system image.", + "Specifies which mode to run '%s' command against. Acceptable values are '%s'." + + " If not set or set to '%s' we generate split, standalone and instant" + + " APKs. If set to '%s' we generate universal APK. If set to '%s' we" + + " generate APKs for system image. If set to %s we generate the archived" + + " APK.", BuildApksCommand.COMMAND_NAME, joinFlagOptions(ApkBuildMode.values()), DEFAULT.getLowerCaseName(), UNIVERSAL.getLowerCaseName(), - SYSTEM.getLowerCaseName()) + SYSTEM.getLowerCaseName(), + ARCHIVE.getLowerCaseName()) .build()) .addFlag( FlagDescription.builder() @@ -938,6 +998,16 @@ public static CommandHelp help() { "The minimum API version for signing the generated APKs with rotation using V3" + " signature scheme.") .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(ROTATION_MINIMUM_SDK_VERSION_FLAG.getName()) + .setExampleValue("30") + .setOptional(true) + .setDescription( + "The minimum Android version for signing the generated APKs with rotation. The" + + " original signing key for the APK will be used for all previous platform" + + " versions.") + .build()) .addFlag( FlagDescription.builder() .setFlagName(LINEAGE_FLAG.getName()) @@ -1075,6 +1145,14 @@ public static CommandHelp help() { "If set, prints extra information about the command execution in the standard" + " output.") .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(P7ZIP_PATH_FLAG.getName()) + .setOptional(true) + .setDescription( + "Path to the 7zip binary to use. Mandatory if Android App Bundle requires 7zip" + + " compression.") + .build()) .addFlag( FlagDescription.builder() .setFlagName(CREATE_STAMP_FLAG.getName()) @@ -1140,6 +1218,16 @@ public static CommandHelp help() { "Name of source generating the stamp. For stores, it is their package names." + " For locally generated stamp, it is 'local'.") .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName()) + .setExampleValue("path/to/bundle1.asb,path/to/bundle2.asb") + .setOptional(true) + .setDescription( + "Experimental flag for specifying paths to SDK bundles for the runtime-enabled" + + " SDKs that the App Bundle depends on, separated by commas. Each SDK" + + " bundle must have an extension .asb.") + .build()) .build(); } @@ -1161,6 +1249,7 @@ private static void populateSigningConfigurationFromFlags( Optional keystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags); Optional keyPassword = KEY_PASSWORD_FLAG.getValue(flags); Optional minV3RotationApi = MINIMUM_V3_ROTATION_API_VERSION_FLAG.getValue(flags); + Optional rotationMinSdkVersion = ROTATION_MINIMUM_SDK_VERSION_FLAG.getValue(flags); if (keystorePath.isPresent() && keyAlias.isPresent()) { SignerConfig signerConfig = @@ -1169,7 +1258,8 @@ private static void populateSigningConfigurationFromFlags( SigningConfiguration.Builder builder = SigningConfiguration.builder() .setSignerConfig(signerConfig) - .setMinimumV3RotationApiVersion(minV3RotationApi); + .setMinimumV3RotationApiVersion(minV3RotationApi) + .setRotationMinSdkVersion(rotationMinSdkVersion); populateLineageFromFlags(builder, flags); buildApksCommand.setSigningConfiguration(builder.build()); } else if (keystorePath.isPresent() && !keyAlias.isPresent()) { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java index a39ec48f..7f7bd63b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -17,6 +17,7 @@ import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.ARCHIVE; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; +import static com.android.tools.build.bundletool.commands.ExtractApksCommand.ALL_MODULES_SHORTCUT; import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -24,15 +25,12 @@ import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Config.BundleConfig; import com.android.bundle.Devices.DeviceSpec; +import com.android.tools.build.bundletool.archive.ArchivedApksGenerator; import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.commands.BuildApksCommand.SystemApkOption; import com.android.tools.build.bundletool.device.ApkMatcher; -import com.android.tools.build.bundletool.internal.HibernatedApksGenerator; import com.android.tools.build.bundletool.io.ApkSerializerManager; -import com.android.tools.build.bundletool.io.ApkSetBuilderFactory; -import com.android.tools.build.bundletool.io.ApkSetBuilderFactory.ApkSetBuilder; -import com.android.tools.build.bundletool.io.SplitApkSerializer; -import com.android.tools.build.bundletool.io.StandaloneApkSerializer; +import com.android.tools.build.bundletool.io.ApkSetWriter; import com.android.tools.build.bundletool.io.TempDirectory; import com.android.tools.build.bundletool.mergers.BundleModuleMerger; import com.android.tools.build.bundletool.model.AppBundle; @@ -45,6 +43,7 @@ import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.targeting.AlternativeVariantTargetingPopulator; +import com.android.tools.build.bundletool.model.utils.LocaleConfigXmlInjector; import com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils; import com.android.tools.build.bundletool.model.utils.SplitsXmlInjector; import com.android.tools.build.bundletool.model.utils.Versions; @@ -61,9 +60,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.Optional; import java.util.stream.Stream; import javax.inject.Inject; @@ -77,13 +79,11 @@ public final class BuildApksManager { private final Optional deviceSpec; private final TempDirectory tempDir; - private final SplitApkSerializer splitApkSerializer; - private final StandaloneApkSerializer standaloneApkSerializer; private final ApkSerializerManager apkSerializerManager; private final SplitApksGenerator splitApksGenerator; private final ShardedApksFacade shardedApksFacade; private final ApkOptimizations apkOptimizations; - private final HibernatedApksGenerator hibernatedApksGenerator; + private final ArchivedApksGenerator archivedApksGenerator; @Inject BuildApksManager( @@ -92,25 +92,21 @@ public final class BuildApksManager { Version bundletoolVersion, Optional deviceSpec, TempDirectory tempDir, - SplitApkSerializer splitApkSerializer, - StandaloneApkSerializer standaloneApkSerializer, ApkSerializerManager apkSerializerManager, SplitApksGenerator splitApksGenerator, ShardedApksFacade shardedApksFacade, ApkOptimizations apkOptimizations, - HibernatedApksGenerator hibernatedApksGenerator) { + ArchivedApksGenerator archivedApksGenerator) { this.appBundle = appBundle; this.command = command; this.bundletoolVersion = bundletoolVersion; this.deviceSpec = deviceSpec; this.tempDir = tempDir; - this.splitApkSerializer = splitApkSerializer; - this.standaloneApkSerializer = standaloneApkSerializer; this.splitApksGenerator = splitApksGenerator; this.apkSerializerManager = apkSerializerManager; this.shardedApksFacade = shardedApksFacade; this.apkOptimizations = apkOptimizations; - this.hibernatedApksGenerator = hibernatedApksGenerator; + this.archivedApksGenerator = archivedApksGenerator; } public void execute() throws IOException { @@ -170,9 +166,9 @@ public void execute() throws IOException { generatedApksBuilder.setSystemApks(generateSystemApks(appBundle, requestedModules)); } - // Hibernated APKs - if (apksToGenerate.generateHibernatedApks()) { - generatedApksBuilder.setHibernatedApks(generateHibernatedApks(appBundle)); + // Archived APKs + if (apksToGenerate.generateArchivedApks()) { + generatedApksBuilder.setArchivedApks(generateArchivedApks(appBundle)); } // Asset Slices @@ -188,8 +184,24 @@ public void execute() throws IOException { ? Optional.empty() : appBundle.getBaseModule().getAndroidManifest().getMaxSdkVersion()); - SplitsXmlInjector splitsXmlInjector = new SplitsXmlInjector(); - generatedApks = splitsXmlInjector.process(generatedApks); + // A variant is a set of APKs. One device is guaranteed to receive only APKs from the same. This + // is why we are processing new entries like split.xml for each variant separately. + generatedApks = + GeneratedApks.fromModuleSplits( + generatedApks.getAllApksGroupedByOrderedVariants().asMap().entrySet().stream() + .map( + keySplit -> { + SplitsXmlInjector splitsXmlInjector = new SplitsXmlInjector(); + ImmutableList moduleSplits = + splitsXmlInjector.process(keySplit.getKey(), keySplit.getValue()); + LocaleConfigXmlInjector localeConfigXmlInjector = + new LocaleConfigXmlInjector(); + moduleSplits = + localeConfigXmlInjector.process(keySplit.getKey(), moduleSplits); + return moduleSplits; + }) + .flatMap(Collection::stream) + .collect(toImmutableList())); if (deviceSpec.isPresent()) { // It is easier to fully check device compatibility once the splits have been generated (in @@ -198,22 +210,18 @@ public void execute() throws IOException { checkDeviceCompatibilityWithBundle(generatedApks, deviceSpec.get()); } - ApkSetBuilder apkSetBuilder = createApkSetBuilder(tempDir.getPath()); + if (command.getOverwriteOutput() && Files.exists(command.getOutputFile())) { + MoreFiles.deleteRecursively(command.getOutputFile(), RecursiveDeleteOption.ALLOW_INSECURE); + } // Create variants and serialize APKs. - apkSerializerManager.populateApkSetBuilder( - apkSetBuilder, + apkSerializerManager.serializeApkSet( + createApkSetWriter(tempDir.getPath()), generatedApks, generatedAssetSlices.build(), - command.getApkBuildMode(), deviceSpec, getLocalTestingInfo(appBundle), permanentlyFusedModules); - - if (command.getOverwriteOutput()) { - Files.deleteIfExists(command.getOutputFile()); - } - apkSetBuilder.writeTo(command.getOutputFile()); } private ImmutableList generateStandaloneApks(AppBundle appBundle) { @@ -277,9 +285,8 @@ private ImmutableList generateSystemApks( getSystemApkOptimizations()); } - private ImmutableList generateHibernatedApks(AppBundle appBundle) - throws IOException { - return ImmutableList.of(hibernatedApksGenerator.generateHibernatedApk(appBundle)); + private ImmutableList generateArchivedApks(AppBundle appBundle) throws IOException { + return ImmutableList.of(archivedApksGenerator.generateArchivedApk(appBundle)); } private static void checkDeviceCompatibilityWithBundle( @@ -288,14 +295,12 @@ private static void checkDeviceCompatibilityWithBundle( generatedApks.getAllApksStream().forEach(apkMatcher::checkCompatibleWithApkTargeting); } - private ApkSetBuilder createApkSetBuilder(Path tempDir) { + private ApkSetWriter createApkSetWriter(Path tempDir) { switch (command.getOutputFormat()) { case APK_SET: - return ApkSetBuilderFactory.createApkSetBuilder( - splitApkSerializer, standaloneApkSerializer, tempDir); + return ApkSetWriter.zip(tempDir, command.getOutputFile()); case DIRECTORY: - return ApkSetBuilderFactory.createApkSetWithoutArchiveBuilder( - splitApkSerializer, standaloneApkSerializer, command.getOutputFile()); + return ApkSetWriter.directory(command.getOutputFile()); } throw InvalidCommandException.builder() .withInternalMessage("Unsupported output format '%s'.", command.getOutputFormat()) @@ -313,6 +318,8 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat apkGenerationConfiguration.setEnableUncompressedNativeLibraries( apkOptimizations.getUncompressNativeLibraries()); + apkGenerationConfiguration.setEnableDexCompressionSplitter( + apkOptimizations.getUncompressDexFiles()); apkGenerationConfiguration.setInstallableOnExternalStorage( appBundle @@ -371,6 +378,9 @@ private static boolean targetsPreL(AppBundle bundle) { private static ImmutableList getBundleModules( AppBundle appBundle, ImmutableSet moduleNames) { + if (moduleNames.contains(ALL_MODULES_SHORTCUT)) { + return appBundle.getModules().values().asList(); + } return moduleNames.stream() .map(BundleModuleName::create) .map(appBundle::getModule) @@ -467,7 +477,7 @@ private void validate() { || generateInstantApks() || generateUniversalApk() || generateSystemApks() - || generateHibernatedApks() + || generateArchivedApks() || generateAssetSlices(); if (!generatesAtLeastOneApk) { throw InvalidCommandException.builder().withInternalMessage("No APKs to generate.").build(); @@ -540,7 +550,7 @@ public boolean generateSystemApks() { return apkBuildMode.equals(SYSTEM); } - public boolean generateHibernatedApks() { + public boolean generateArchivedApks() { if (appBundle.isApex() || appBundle.isAssetOnly()) { return false; } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManagerComponent.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManagerComponent.java index 11d6453f..8162948b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManagerComponent.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManagerComponent.java @@ -27,7 +27,7 @@ /** Dagger component to create a {@link BuildApksManager}. */ @CommandScoped -@Component(modules = BuildApksModule.class) +@Component(modules = {BuildApksModule.class, AppBundleModule.class}) public interface BuildApksManagerComponent { BuildApksManager create(); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java index 43df81d2..1efbe79d 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java @@ -47,7 +47,6 @@ includes = { BundleConfigModule.class, BundletoolModule.class, - AppBundleModule.class, ApkSerializerModule.class }) public final class BuildApksModule { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java new file mode 100644 index 00000000..ba57b06c --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.commands.BuildSdkApksCommand.OutputFormat.APK_SET; +import static com.android.tools.build.bundletool.commands.BuildSdkApksCommand.OutputFormat.DIRECTORY; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; +import static com.google.common.base.Preconditions.checkArgument; + +import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; +import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; +import com.android.tools.build.bundletool.flags.Flag; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.io.ZipReader; +import com.android.tools.build.bundletool.model.Password; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.SignerConfig; +import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; +import com.android.tools.build.bundletool.validation.SdkBundleValidator; +import com.google.auto.value.AutoValue; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +/** Command to generate APKs from an Android SDK Bundle. */ +@AutoValue +public abstract class BuildSdkApksCommand { + + private static final int DEFAULT_THREAD_POOL_SIZE = 4; + + public static final String COMMAND_NAME = "build-sdk-apks"; + + private static final Integer DEFAULT_SDK_VERSION_CODE = 1; + + enum OutputFormat { + /** Generated APKs are stored inside created APK Set archive. */ + APK_SET, + /** Generated APKs are stored inside specified directory. */ + DIRECTORY + } + + private static final Flag SDK_BUNDLE_LOCATION_FLAG = Flag.path("sdk-bundle"); + private static final Flag VERSION_CODE_FLAG = Flag.positiveInteger("version-code"); + private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); + private static final Flag OUTPUT_FORMAT_FLAG = + Flag.enumFlag("output-format", OutputFormat.class); + private static final Flag OVERWRITE_OUTPUT_FLAG = Flag.booleanFlag("overwrite"); + private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); + private static final Flag MAX_THREADS_FLAG = Flag.positiveInteger("max-threads"); + private static final Flag VERBOSE_FLAG = Flag.booleanFlag("verbose"); + + // Signing-related flags: should match flags from apksig library. + private static final Flag KEYSTORE_FLAG = Flag.path("ks"); + private static final Flag KEY_ALIAS_FLAG = Flag.string("ks-key-alias"); + private static final Flag KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass"); + private static final Flag KEY_PASSWORD_FLAG = Flag.password("key-pass"); + + private static final SystemEnvironmentProvider DEFAULT_PROVIDER = + new DefaultSystemEnvironmentProvider(); + + abstract Path getSdkBundlePath(); + + abstract Integer getVersionCode(); + + abstract Path getOutputFile(); + + abstract boolean getOverwriteOutput(); + + ListeningExecutorService getExecutorService() { + return getExecutorServiceInternal(); + } + + abstract ListeningExecutorService getExecutorServiceInternal(); + + public abstract boolean getVerbose(); + + abstract boolean isExecutorServiceCreatedByBundleTool(); + + abstract OutputFormat getOutputFormat(); + + abstract Optional getAapt2Command(); + + abstract Optional getSigningConfiguration(); + + static BuildSdkApksCommand.Builder builder() { + return new AutoValue_BuildSdkApksCommand.Builder() + .setOverwriteOutput(false) + .setOutputFormat(APK_SET) + .setVersionCode(DEFAULT_SDK_VERSION_CODE) + .setVerbose(false); + } + + /** Builder for the {@link BuildSdkApksCommand}. */ + @AutoValue.Builder + abstract static class Builder { + /** Sets the path to the input SDK bundle. Must have the extension ".asb". */ + abstract Builder setSdkBundlePath(Path sdkBundlePath); + + /** Sets the SDK version code */ + abstract Builder setVersionCode(Integer versionCode); + + /** + * Sets path to the output produced by the command. Depends on the output format: + * + *

    + *
  • 'APK_SET', path to where the APK Set must be generated. Must have the extension + * ".apks". + *
  • 'DIRECTORY', path to the directory where generated APKs will be stored. + *
+ */ + abstract Builder setOutputFile(Path outputFile); + + /** + * Sets whether to overwrite the contents of the output file. + * + *

The default is {@code false}. If set to {@code false} and the output file is present, + * exception is thrown. + */ + public abstract Builder setOverwriteOutput(boolean overwriteOutput); + + /** Sets the output format. */ + abstract Builder setOutputFormat(OutputFormat outputFormat); + + /** Provides a wrapper around the execution of the aapt2 command. */ + abstract Builder setAapt2Command(Aapt2Command aapt2Command); + + /** Sets the signing configuration to be used for all generated APKs. */ + abstract Builder setSigningConfiguration(SigningConfiguration signingConfiguration); + + /** + * Allows to set an executor service for parallelization. + * + *

Optional. The caller is responsible for providing a service that accepts new tasks, and + * for shutting it down afterwards. + */ + Builder setExecutorService(ListeningExecutorService executorService) { + setExecutorServiceInternal(executorService); + setExecutorServiceCreatedByBundleTool(false); + return this; + } + + abstract Builder setExecutorServiceInternal(ListeningExecutorService executorService); + + abstract Optional getExecutorServiceInternal(); + + /** + * Sets whether the ExecutorService has been created by bundletool, otherwise provided by the + * client. + * + *

If true, the ExecutorService is shut down at the end of execution of this command. + */ + abstract Builder setExecutorServiceCreatedByBundleTool(boolean value); + + /** + * Sets whether to display verbose information about what is happening during the command + * execution. + */ + public abstract Builder setVerbose(boolean enableVerbose); + + abstract BuildSdkApksCommand autoBuild(); + + BuildSdkApksCommand build() { + if (!getExecutorServiceInternal().isPresent()) { + setExecutorServiceInternal(createInternalExecutorService(DEFAULT_THREAD_POOL_SIZE)); + setExecutorServiceCreatedByBundleTool(true); + } + return autoBuild(); + } + } + + public static BuildSdkApksCommand fromFlags(ParsedFlags flags) { + return fromFlags(flags, System.out, DEFAULT_PROVIDER); + } + + static BuildSdkApksCommand fromFlags( + ParsedFlags flags, PrintStream out, SystemEnvironmentProvider provider) { + Builder sdkApksCommandBuilder = + BuildSdkApksCommand.builder() + .setSdkBundlePath(SDK_BUNDLE_LOCATION_FLAG.getRequiredValue(flags)) + .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); + + // Optional arguments. + OUTPUT_FORMAT_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setOutputFormat); + OVERWRITE_OUTPUT_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setOverwriteOutput); + VERSION_CODE_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setVersionCode); + AAPT2_PATH_FLAG + .getValue(flags) + .ifPresent( + aapt2Path -> + sdkApksCommandBuilder.setAapt2Command( + Aapt2Command.createFromExecutablePath(aapt2Path))); + MAX_THREADS_FLAG + .getValue(flags) + .ifPresent( + maxThreads -> + sdkApksCommandBuilder + .setExecutorService(createInternalExecutorService(maxThreads)) + .setExecutorServiceCreatedByBundleTool(true)); + VERBOSE_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setVerbose); + + populateSigningConfigurationFromFlags(sdkApksCommandBuilder, flags, out, provider); + + flags.checkNoUnknownFlags(); + + return sdkApksCommandBuilder.build(); + } + + public void execute() { + validateInput(); + + try (ZipFile bundleZip = new ZipFile(getSdkBundlePath().toFile()); + ZipReader zipReader = ZipReader.createFromFile(getSdkBundlePath()); + TempDirectory tempDir = new TempDirectory(getClass().getSimpleName())) { + + SdkBundleValidator bundleValidator = SdkBundleValidator.create(); + bundleValidator.validateFile(bundleZip); + + SdkBundle sdkBundle = SdkBundle.buildFromZip(bundleZip, getVersionCode()); + bundleValidator.validate(sdkBundle); + + DaggerBuildSdkApksManagerComponent.builder() + .setBuildSdkApksCommand(this) + .setTempDirectory(tempDir) + .setSdkBundle(sdkBundle) + .setZipReader(zipReader) + .setUseBundleCompression(false) + .build() + .create() + .execute(); + + } catch (ZipException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage("The SDK Bundle is not a valid zip file.") + .build(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when validating the Sdk Bundle.", e); + } finally { + if (isExecutorServiceCreatedByBundleTool()) { + getExecutorService().shutdown(); + } + } + } + + private void validateInput() { + FilePreconditions.checkFileExistsAndReadable(getSdkBundlePath()); + FilePreconditions.checkFileHasExtension("ASB file", getSdkBundlePath(), ".asb"); + + switch (getOutputFormat()) { + case APK_SET: + if (!getOverwriteOutput()) { + checkFileDoesNotExist(getOutputFile()); + } + break; + case DIRECTORY: + if (getOverwriteOutput()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "'%s' flag is not supported for '%s' output format.", + OVERWRITE_OUTPUT_FLAG.getName(), BuildSdkApksCommand.OutputFormat.DIRECTORY) + .build(); + } + break; + } + } + + /** + * Creates an internal executor service that uses at most the given number of threads. + * + *

The caller is responsible for shutting down the executor service. + */ + private static ListeningExecutorService createInternalExecutorService(int maxThreads) { + checkArgument(maxThreads >= 0, "The maxThreads must be positive, got %s.", maxThreads); + return MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(maxThreads)); + } + + public static CommandHelp help() { + return CommandHelp.builder() + .setCommandName(COMMAND_NAME) + .setCommandDescription( + CommandDescription.builder() + .setShortDescription("Generates APKs from an Android SDK Bundle.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(SDK_BUNDLE_LOCATION_FLAG.getName()) + .setExampleValue("path/to/SDKbundle.asb") + .setDescription("Path to SDK bundle. Must have the extension '.asb'.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(VERSION_CODE_FLAG.getName()) + .setExampleValue("1") + .setOptional(true) + .setDescription("SDK version code") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OUTPUT_FILE_FLAG.getName()) + .setExampleValue("output.apks") + .setDescription( + "Path to where the APK Set archive should be created (default) or path to the" + + " directory where generated APKs should be stored when flag --%s is set" + + " to '%s'.", + OUTPUT_FORMAT_FLAG.getName(), DIRECTORY) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OUTPUT_FORMAT_FLAG.getName()) + .setExampleValue("apk_set|directory") + .setOptional(true) + .setDescription( + "Specifies output format for generated APKs. If set to '%s'" + + " outputs APKs into the created APK Set archive (default). If set to '%s'" + + " outputs APKs into the specified directory.", + APK_SET, DIRECTORY) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OVERWRITE_OUTPUT_FLAG.getName()) + .setOptional(true) + .setDescription("If set, any previous existing output will be overwritten.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(AAPT2_PATH_FLAG.getName()) + .setExampleValue("path/to/aapt2") + .setOptional(true) + .setDescription("Path to the aapt2 binary to use.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(VERBOSE_FLAG.getName()) + .setOptional(true) + .setDescription( + "If set, prints extra information about the command execution in the standard" + + " output.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEYSTORE_FLAG.getName()) + .setExampleValue("path/to/keystore") + .setDescription( + "Path to the keystore that should be used to sign the generated APKs.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEY_ALIAS_FLAG.getName()) + .setExampleValue("key-alias") + .setDescription( + "Alias of the key to use in the keystore to sign the generated APKs.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEYSTORE_PASSWORD_FLAG.getName()) + .setExampleValue("[pass|file]:value") + .setOptional(true) + .setDescription( + "Password of the keystore to use to sign the generated APKs. If provided, must " + + "be prefixed with either 'pass:' (if the password is passed in clear " + + "text, e.g. 'pass:qwerty') or 'file:' (if the password is the first line " + + "of a file, e.g. 'file:/tmp/myPassword.txt'). If this flag is not set, " + + "the password will be requested on the prompt.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEY_PASSWORD_FLAG.getName()) + .setExampleValue("key-password") + .setOptional(true) + .setDescription( + "Password of the key in the keystore to use to sign the generated APKs. If " + + "provided, must be prefixed with either 'pass:' (if the password is " + + "passed in clear text, e.g. 'pass:qwerty') or 'file:' (if the password " + + "is the first line of a file, e.g. 'file:/tmp/myPassword.txt'). If this " + + "flag is not set, the keystore password will be tried. If that fails, " + + "the password will be requested on the prompt.") + .build()) + .build(); + } + + private static void populateSigningConfigurationFromFlags( + Builder buildSdkApksCommand, + ParsedFlags flags, + PrintStream out, + SystemEnvironmentProvider provider) { + // Signing-related arguments. + Optional keystorePath = KEYSTORE_FLAG.getValue(flags); + Optional keyAlias = KEY_ALIAS_FLAG.getValue(flags); + Optional keystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags); + Optional keyPassword = KEY_PASSWORD_FLAG.getValue(flags); + + if (keystorePath.isPresent() && keyAlias.isPresent()) { + SignerConfig signerConfig = + SignerConfig.extractFromKeystore( + keystorePath.get(), keyAlias.get(), keystorePassword, keyPassword); + SigningConfiguration.Builder builder = + SigningConfiguration.builder().setSignerConfig(signerConfig); + buildSdkApksCommand.setSigningConfiguration(builder.build()); + } else if (keystorePath.isPresent() && !keyAlias.isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Flag --ks-key-alias is required when --ks is set.") + .build(); + } else if (!keystorePath.isPresent() && keyAlias.isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Flag --ks is required when --ks-key-alias is set.") + .build(); + } else { + // Try to use debug keystore if present. + Optional debugConfig = + DebugKeystoreUtils.getDebugSigningConfiguration(provider); + if (debugConfig.isPresent()) { + out.printf( + "INFO: The APKs will be signed with the debug keystore found at '%s'.%n", + DebugKeystoreUtils.DEBUG_KEYSTORE_CACHE.getUnchecked(provider).get()); + buildSdkApksCommand.setSigningConfiguration(debugConfig.get()); + } else { + out.println( + "WARNING: The APKs won't be signed and thus not installable unless you also pass a " + + "keystore via the flag --ks. See the command help for more information."); + } + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManager.java new file mode 100644 index 00000000..27f981f4 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManager.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.tools.build.bundletool.io.ApkSerializerManager; +import com.android.tools.build.bundletool.io.ApkSetWriter; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.GeneratedApks; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.android.tools.build.bundletool.shards.ModuleSplitterForShards; +import com.android.tools.build.bundletool.shards.StandaloneApksGenerator; +import com.google.common.collect.ImmutableList; +import javax.inject.Inject; + +/** Executes the "build-sdk-apks" command. */ +public class BuildSdkApksManager { + private final ApkSerializerManager apkSerializerManager; + private final BuildSdkApksCommand command; + private final TempDirectory tempDirectory; + private final SdkBundle sdkBundle; + private final ApkOptimizations apkOptimizations; + private final ModuleSplitterForShards moduleSplitterForShards; + + @Inject + BuildSdkApksManager( + ApkSerializerManager apkSerializerManager, + BuildSdkApksCommand command, + TempDirectory tempDirectory, + SdkBundle sdkBundle, + ApkOptimizations apkOptimizations, + ModuleSplitterForShards moduleSplitterForShards) { + this.apkSerializerManager = apkSerializerManager; + this.command = command; + this.tempDirectory = tempDirectory; + this.sdkBundle = sdkBundle; + this.apkOptimizations = apkOptimizations; + this.moduleSplitterForShards = moduleSplitterForShards; + } + + void execute() { + ImmutableList sdkApks = generateSdkApks(); + GeneratedApks generatedApks = GeneratedApks.builder().setStandaloneApks(sdkApks).build(); + + // Create variants and serialize APKs. + apkSerializerManager.serializeSdkApkSet(createApkSetWriter(), generatedApks); + } + + private ApkSetWriter createApkSetWriter() { + switch (command.getOutputFormat()) { + case APK_SET: + return ApkSetWriter.zip(tempDirectory.getPath(), command.getOutputFile()); + case DIRECTORY: + return ApkSetWriter.directory(command.getOutputFile()); + } + throw InvalidCommandException.builder() + .withInternalMessage("Unsupported output format '%s'.", command.getOutputFormat()) + .build(); + } + + private ImmutableList generateSdkApks() { + return moduleSplitterForShards + .generateSplits(sdkBundle.getModule(), apkOptimizations.getStandaloneDimensions()) + .stream() + .map(StandaloneApksGenerator::setVariantTargetingAndSplitType) + .map( + moduleSplit -> + moduleSplit.writeSdkVersionName( + sdkBundle.getMajorVersion() + ".0." + sdkBundle.getPatchVersion())) + .map(moduleSplit -> moduleSplit.writeSdkVersionCode(sdkBundle.getVersionCode())) + .map(ModuleSplit::overrideMinSdkVersionForSdkSandbox) + .map(ModuleSplit::addDefaultPatchVersionIfNotSet) + .collect(toImmutableList()); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerComponent.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerComponent.java new file mode 100644 index 00000000..860fc8c7 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerComponent.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import com.android.tools.build.bundletool.commands.BuildApksManagerComponent.UseBundleCompression; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.io.ZipReader; +import com.android.tools.build.bundletool.model.SdkBundle; +import dagger.BindsInstance; +import dagger.Component; + +/** Dagger component to create a {@link BuildSdkApksManager}. */ +@Component(modules = {BuildSdkApksModule.class}) +public interface BuildSdkApksManagerComponent { + BuildSdkApksManager create(); + + /** Builder for the {@link BuildSdkApksManagerComponent}. */ + @Component.Builder + interface Builder { + BuildSdkApksManagerComponent build(); + + @BindsInstance + Builder setTempDirectory(TempDirectory tempDirectory); + + @BindsInstance + Builder setBuildSdkApksCommand(BuildSdkApksCommand command); + + @BindsInstance + Builder setSdkBundle(SdkBundle appBundle); + + @BindsInstance + Builder setZipReader(ZipReader zipReader); + + @BindsInstance + Builder setUseBundleCompression(@UseBundleCompression boolean useBundleCompression); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksModule.java new file mode 100644 index 00000000..fdbf236e --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksModule.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Devices.DeviceSpec; +import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; +import com.android.tools.build.bundletool.io.ApkSerializer; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.io.ZipFlingerApkSerializer; +import com.android.tools.build.bundletool.model.ApkListener; +import com.android.tools.build.bundletool.model.ApkModifier; +import com.android.tools.build.bundletool.model.Bundle; +import com.android.tools.build.bundletool.model.DefaultSigningConfigurationProvider; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.SigningConfigurationProvider; +import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.google.common.util.concurrent.ListeningExecutorService; +import dagger.Binds; +import dagger.BindsOptionalOf; +import dagger.Module; +import dagger.Provides; +import java.util.Optional; + +/** Dagger module for the build-sdk-apks command. */ +@Module +public abstract class BuildSdkApksModule { + + @Provides + static Aapt2Command provideAapt2Command(BuildSdkApksCommand command, TempDirectory tempDir) { + return command + .getAapt2Command() + .orElseGet(() -> CommandUtils.extractAapt2FromJar(tempDir.getPath())); + } + + @Provides + static Version provideBundletoolVersion(BundleConfig bundleConfig) { + return Version.of(bundleConfig.getBundletool().getVersion()); + } + + @Provides + static BundleConfig provideBundleConfig(SdkBundle sdkBundle) { + return sdkBundle.getBundleConfig(); + } + + @Binds + abstract Bundle bundle(SdkBundle bundle); + + @BindsOptionalOf + @BuildApksModule.StampSigningConfig + abstract SigningConfiguration bindOptionalSigningConfiguration(); + + @Provides + @BuildApksModule.ApkSigningConfigProvider + static Optional provideApkSigningConfigurationProvider( + BuildSdkApksCommand command, Version version) { + return command + .getSigningConfiguration() + .map(signingConfig -> new DefaultSigningConfigurationProvider(signingConfig, version)); + } + + @BindsOptionalOf + abstract DeviceSpec bindOptionalDeviceSpec(); + + @Provides + static ListeningExecutorService provideExecutorService(BuildSdkApksCommand command) { + return command.getExecutorService(); + } + + @BindsOptionalOf + abstract ApkListener bindOptionalApkListener(); + + @BindsOptionalOf + abstract ApkModifier bindOptionalApkModifier(); + + @Provides + static ApkOptimizations provideApkOptimizations() { + return ApkOptimizations.getOptimizationsForUniversalApk(); + } + + @Provides + static ApkBuildMode provideApkBuildMode() { + return ApkBuildMode.DEFAULT; + } + + @BuildApksModule.FirstVariantNumber + @Provides + static Optional provideFirstVariantNumber() { + return Optional.of(0); + } + + @Provides + @BuildApksModule.VerboseLogs + static boolean provideVerbose(BuildSdkApksCommand command) { + return command.getVerbose(); + } + + @Binds + abstract ApkSerializer apkSerializerHelper(ZipFlingerApkSerializer apkSerializerHelper); +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/CommandHelp.java b/src/main/java/com/android/tools/build/bundletool/commands/CommandHelp.java index 965846db..fbcf4996 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/CommandHelp.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/CommandHelp.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; import com.google.errorprone.annotations.Immutable; @@ -33,7 +34,6 @@ import java.util.Locale; import java.util.Optional; import java.util.StringJoiner; -import javax.annotation.CheckReturnValue; /** Helper to print command helps in the console. */ @Immutable diff --git a/src/main/java/com/android/tools/build/bundletool/flags/Flag.java b/src/main/java/com/android/tools/build/bundletool/flags/Flag.java index 46e84c4a..954997f7 100644 --- a/src/main/java/com/android/tools/build/bundletool/flags/Flag.java +++ b/src/main/java/com/android/tools/build/bundletool/flags/Flag.java @@ -94,6 +94,11 @@ public static Flag> pathList(String name) { return new ListFlag<>(new PathFlag(name)); } + /** Path flag holding a set of comma-delimited values. */ + public static Flag> pathSet(String name) { + return new SetFlag<>(new PathFlag(name)); + } + /** Positive integer flag holding a single value. */ public static Flag positiveInteger(String name) { return new IntegerFlag( diff --git a/src/main/java/com/android/tools/build/bundletool/io/Aapt2ResourceConverter.java b/src/main/java/com/android/tools/build/bundletool/io/Aapt2ResourceConverter.java new file mode 100644 index 00000000..3f21c75d --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/Aapt2ResourceConverter.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Streams.stream; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Config.ResourceOptimizations.SparseEncoding; +import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.utils.ZipUtils; +import com.android.zipflinger.ZipArchive; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.zip.ZipFile; +import javax.inject.Inject; + +/** Class to convert module splits from proto to binary format. */ +class Aapt2ResourceConverter { + + private final Aapt2Command aapt2Command; + private final boolean enableSparseEncoding; + private final ListeningExecutorService executorService; + + @Inject + Aapt2ResourceConverter( + Aapt2Command aapt2Command, + ListeningExecutorService executorService, + BundleConfig bundleConfig) { + this.aapt2Command = aapt2Command; + this.executorService = executorService; + this.enableSparseEncoding = + bundleConfig + .getOptimizations() + .getResourceOptimizations() + .getSparseEncoding() + .equals(SparseEncoding.ENFORCED); + } + + /** + * Converts module splits from proto format to binary format via invoking 'aapt2 convert'. + * + *

Returns a list of {@link ModuleSplit} with converted entries in the same order as in + * original {@code allSplits} list. + */ + public ImmutableList convert( + Collection allSplits, SerializationFilesManager filesManager) { + // Uncompress all resource entries we have in module splits and store them in uncompressed + // form inside special zip pack. This is done because we may have the same entry duplicated + // in multiple splits to uncompress them only once. + ModuleEntriesPacker packer = + new ModuleEntriesPacker(filesManager.getResourcesEntriesPackPath(), /* namePrefix= */ "r_"); + allSplits.stream() + .flatMap(split -> split.getEntries().stream()) + .filter( + entry -> + ApkSerializerHelper.requiresAapt2Conversion( + ApkSerializerHelper.toApkEntryPath(entry.getPath()))) + .forEach(packer::add); + ModuleEntriesPack allResourcesUncompressedPack = packer.pack(Zipper.uncompressedZip()); + + ResourceConverter resourceConverter = + new ResourceConverter(filesManager, allResourcesUncompressedPack); + + ImmutableList> binarySplitFutures = + allSplits.stream() + .map( + split -> + executorService.submit(() -> resourceConverter.convertResourcesToBinary(split))) + .collect(toImmutableList()); + return ConcurrencyUtils.waitForAll(binarySplitFutures); + } + + private class ResourceConverter { + + private final SerializationFilesManager filesManager; + private final ModuleEntriesPack packWithResourceEntries; + + ResourceConverter( + SerializationFilesManager filesManager, ModuleEntriesPack packWithResourceEntries) { + this.filesManager = filesManager; + this.packWithResourceEntries = packWithResourceEntries; + } + + /** Converts resources in split from proto to binary format. */ + public ModuleSplit convertResourcesToBinary(ModuleSplit split) { + try { + Path protoApkPath = writePartialProtoApk(split); + Path binaryApkPath = convertAndOptimizeProtoApk(split, protoApkPath); + Files.delete(protoApkPath); + + return withConvertedEntries(split, binaryApkPath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** Writes APK with only resource entries in proto format. */ + private Path writePartialProtoApk(ModuleSplit split) throws IOException { + Path protoApkPath = filesManager.getNextAapt2ProtoApkPath(); + try (ZipArchive protoApk = new ZipArchive(protoApkPath)) { + ImmutableList entriesToConvert = + split.getEntries().stream() + .filter( + entry -> + ApkSerializerHelper.requiresAapt2Conversion( + ApkSerializerHelper.toApkEntryPath(entry.getPath()))) + .collect(toImmutableList()); + protoApk.add( + packWithResourceEntries.select( + entriesToConvert, + entry -> ApkSerializerHelper.toApkEntryPath(entry.getPath()).toString())); + } + return protoApkPath; + } + + /** + * Invokes 'aapt2' convert and optimize. Returns path to APK with resources in binary format. + */ + private Path convertAndOptimizeProtoApk(ModuleSplit split, Path protoApkPath) { + Path binaryApkPath = filesManager.getNextAapt2BinaryApkPath(); + if (enableSparseEncoding && split.getResourceTable().isPresent()) { + Path interimApkPath = filesManager.getNextAapt2BinaryApkPath(); + aapt2Command.convertApkProtoToBinary(protoApkPath, interimApkPath); + aapt2Command.optimizeToSparseResourceTables(interimApkPath, binaryApkPath); + return binaryApkPath; + } + aapt2Command.convertApkProtoToBinary(protoApkPath, binaryApkPath); + return binaryApkPath; + } + + /** + * Replaces resource entries in original {@link ModuleSplit} with entries from converted APK. + */ + private ModuleSplit withConvertedEntries(ModuleSplit split, Path binaryApkPath) { + ZipFile binaryZip = filesManager.openBinaryApk(binaryApkPath); + + Stream otherEntriesStream = + split.getEntries().stream() + .filter( + entry -> + !ApkSerializerHelper.requiresAapt2Conversion( + ApkSerializerHelper.toApkEntryPath(entry.getPath()))); + + Stream resourceEntriesStream = + binaryZip.stream() + .filter(zipEntry -> zipEntry.getName().startsWith("res/")) + .map( + zipEntry -> + ModuleEntry.builder() + .setPath(ZipPath.create(zipEntry.getName())) + .setContent(ZipUtils.asByteSource(binaryZip, zipEntry)) + .build()); + + ModuleEntry manifestEntry = + ModuleEntry.builder() + .setContent(ZipUtils.asByteSource(binaryZip, binaryZip.getEntry(MANIFEST_FILENAME))) + .setPath(SpecialModuleEntry.ANDROID_MANIFEST.getPath()) + .setForceUncompressed(false) + .build(); + + Optional resourceTableEntry = + Optional.ofNullable(binaryZip.getEntry("resources.arsc")) + .map( + zipEntry -> + ModuleEntry.builder() + .setContent(ZipUtils.asByteSource(binaryZip, zipEntry)) + .setPath(SpecialModuleEntry.RESOURCE_TABLE.getPath()) + .setForceUncompressed(true) + .build()); + + ImmutableList allEntries = + Streams.concat( + Stream.of(manifestEntry), + stream(resourceTableEntry), + resourceEntriesStream, + otherEntriesStream) + .collect(toImmutableList()); + return split.toBuilder().setEntries(allEntries).build(); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java new file mode 100644 index 00000000..06e8d879 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import com.android.bundle.Commands.ApexApkMetadata; +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.ArchivedApkMetadata; +import com.android.bundle.Commands.SplitApkMetadata; +import com.android.bundle.Commands.StandaloneApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ZipPath; + +/** Helper class which creates ApkDescription for module splits. */ +class ApkDescriptionHelper { + + static ApkDescription createApkDescription(ZipPath relativePath, ModuleSplit split) { + ApkDescription.Builder resultBuilder = + ApkDescription.newBuilder() + .setPath(relativePath.toString()) + .setTargeting(split.getApkTargeting()); + switch (split.getSplitType()) { + case INSTANT: + resultBuilder.setInstantApkMetadata(createSplitApkMetadata(split)); + break; + case SPLIT: + resultBuilder.setSplitApkMetadata(createSplitApkMetadata(split)); + break; + case SYSTEM: + if (split.isBaseModuleSplit() && split.isMasterSplit()) { + resultBuilder.setSystemApkMetadata(createSystemApkMetadata(split)); + } else { + resultBuilder.setSplitApkMetadata(createSplitApkMetadata(split)); + } + break; + case STANDALONE: + if (split.isApex()) { + resultBuilder.setApexApkMetadata(createApexApkMetadata(split)); + } else { + resultBuilder.setStandaloneApkMetadata(createStandaloneApkMetadata(split)); + } + break; + case ASSET_SLICE: + resultBuilder.setAssetSliceMetadata(createSplitApkMetadata(split)); + break; + case ARCHIVE: + resultBuilder.setArchivedApkMetadata(ArchivedApkMetadata.getDefaultInstance()); + break; + } + return resultBuilder.build(); + } + + private static SplitApkMetadata createSplitApkMetadata(ModuleSplit split) { + return SplitApkMetadata.newBuilder() + .setSplitId(split.getAndroidManifest().getSplitId().orElse("")) + .setIsMasterSplit(split.isMasterSplit()) + .build(); + } + + private static SystemApkMetadata createSystemApkMetadata(ModuleSplit split) { + return SystemApkMetadata.newBuilder() + .addAllFusedModuleName(split.getAndroidManifest().getFusedModuleNames()) + .build(); + } + + private static StandaloneApkMetadata createStandaloneApkMetadata(ModuleSplit split) { + return StandaloneApkMetadata.newBuilder() + .addAllFusedModuleName(split.getAndroidManifest().getFusedModuleNames()) + .build(); + } + + private static ApexApkMetadata createApexApkMetadata(ModuleSplit split) { + return ApexApkMetadata.newBuilder() + .addAllApexEmbeddedApkConfig(split.getApexEmbeddedApkConfigs()) + .build(); + } + + private ApkDescriptionHelper() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java index cd852f72..55c6051d 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java @@ -17,6 +17,7 @@ import static java.util.stream.Collectors.toList; +import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.ZipPath; @@ -51,12 +52,16 @@ public class ApkPathManager { private static final Joiner NAME_PARTS_JOINER = Joiner.on('-'); + private final ApkBuildMode apkBuildMode; + /** Paths of APKs that have already been allocated. */ @GuardedBy("this") private final Set usedPaths = new HashSet<>(); @Inject - ApkPathManager() {} + ApkPathManager(ApkBuildMode apkBuildMode) { + this.apkBuildMode = apkBuildMode; + } /** * Returns a unique file path for the given ModuleSplit. @@ -65,6 +70,9 @@ public class ApkPathManager { * each returned value is unique. */ public ZipPath getApkPath(ModuleSplit moduleSplit) { + if (apkBuildMode.equals(ApkBuildMode.UNIVERSAL)) { + return ZipPath.create("universal.apk"); + } String moduleName = moduleSplit.getModuleName().getName(); String targetingSuffix = getTargetingSuffix(moduleSplit); @@ -96,9 +104,9 @@ public ZipPath getApkPath(ModuleSplit moduleSplit) { directory = ZipPath.create("asset-slices"); apkFileName = buildName(moduleName, targetingSuffix); break; - case HIBERNATION: - directory = ZipPath.create("hibernation"); - apkFileName = buildName("hibernation"); + case ARCHIVE: + directory = ZipPath.create("archive"); + apkFileName = buildName("archive"); break; default: throw new IllegalStateException("Unrecognized split type: " + moduleSplit.getSplitType()); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializer.java new file mode 100644 index 00000000..0f84a498 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializer.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import com.android.bundle.Commands.ApkDescription; +import com.android.tools.build.bundletool.model.ApkListener; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ZipPath; +import com.google.common.collect.ImmutableMap; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +/** Serializes APKs on disk. */ +public abstract class ApkSerializer { + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final ApkListener apkListener; + private final boolean verbose; + + ApkSerializer(Optional apkListener, boolean verbose) { + this.apkListener = apkListener.orElse(ApkListener.NO_OP); + this.verbose = verbose; + } + + public ApkDescription serialize(Path apkPath, ModuleSplit moduleSplit) { + return serialize(apkPath.getParent(), apkPath.getFileName().toString(), moduleSplit); + } + + public ApkDescription serialize( + Path outputDirectory, String apkRelativePath, ModuleSplit moduleSplit) { + ZipPath relativePath = ZipPath.create(apkRelativePath); + return serialize(outputDirectory, ImmutableMap.of(relativePath, moduleSplit)).get(relativePath); + } + + public abstract ImmutableMap serialize( + Path outputDirectory, ImmutableMap splitsByRelativePath); + + protected void notifyApkSerialized( + ApkDescription apkDescription, ModuleSplit.SplitType splitType) { + apkListener.onApkFinalized(apkDescription); + + if (verbose) { + System.out.printf( + "INFO: [%s] '%s' of type '%s' was written to disk.%n", + LocalDateTime.now(ZoneId.systemDefault()).format(DATE_FORMATTER), + apkDescription.getPath(), + splitType); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java index 2aba2fa8..3eb80972 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java @@ -26,22 +26,17 @@ import static com.google.common.base.Preconditions.checkArgument; import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; -import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableSet; -import java.nio.file.Path; - -/** Serializes APKs to Proto or Binary format. */ -public abstract class ApkSerializerHelper { +/** Helper methods for APK serialization. */ +public final class ApkSerializerHelper { static final ImmutableSet NO_COMPRESSION_EXTENSIONS = ImmutableSet.of( "3g2", "3gp", "3gpp", "3gpp2", "aac", "amr", "awb", "gif", "imy", "jet", "jpeg", "jpg", "m4a", "m4v", "mid", "midi", "mkv", "mp2", "mp3", "mp4", "mpeg", "mpg", "ogg", "png", "rtttl", "smf", "wav", "webm", "wma", "wmv", "xmf"); - public abstract Path writeToZipFile(ModuleSplit split, Path outputPath); - /** * Transforms the entry path in the module to the final path in the module split. * @@ -50,6 +45,20 @@ public abstract class ApkSerializerHelper { * images or "apex_build_info.pb" for APEX build info. There should only be one such entry. */ public static ZipPath toApkEntryPath(ZipPath pathInModule) { + return toApkEntryPath(pathInModule, /* binaryApk= */ false); + } + + /** + * Transforms the entry path in the module to the final path in the proto or binary APK. + * + *

The entries from root/, dex/, manifest/ directories will be moved to the top level of the + * split. Entries from apex/ will be moved to the top level and named "apex_payload.img" for + * images or "apex_build_info.pb" for APEX build info. There should only be one such entry. + */ + public static ZipPath toApkEntryPath(ZipPath pathInModule, boolean binaryApk) { + if (binaryApk && pathInModule.equals(SpecialModuleEntry.RESOURCE_TABLE.getPath())) { + return ZipPath.create("resources.arsc"); + } if (pathInModule.startsWith(MANIFEST_DIRECTORY)) { checkArgument( pathInModule.getNameCount() == 2, @@ -98,4 +107,6 @@ public static boolean requiresAapt2Conversion(ZipPath path) { || path.equals(SpecialModuleEntry.RESOURCE_TABLE.getPath()) || path.equals(ZipPath.create(MANIFEST_FILENAME)); } + + private ApkSerializerHelper() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java index a621eeff..3360a7fa 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -16,15 +16,14 @@ package com.android.tools.build.bundletool.io; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; -import static com.android.tools.build.bundletool.io.ConcurrencyUtils.waitForAll; import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingByDeterministic; import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingBySortedKeys; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Predicates.alwaysTrue; +import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.util.function.Function.identity; -import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.mapping; import com.android.bundle.Commands.ApkDescription; @@ -33,11 +32,13 @@ import com.android.bundle.Commands.AssetModulesInfo; import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Commands.BuildSdkApksResult; import com.android.bundle.Commands.DefaultTargetingValue; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.InstantMetadata; import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Commands.PermanentlyFusedModule; +import com.android.bundle.Commands.SdkVersionInformation; import com.android.bundle.Commands.Variant; import com.android.bundle.Config.AssetModulesConfig; import com.android.bundle.Config.BundleConfig; @@ -50,12 +51,10 @@ import com.android.tools.build.bundletool.commands.BuildApksModule.FirstVariantNumber; import com.android.tools.build.bundletool.commands.BuildApksModule.VerboseLogs; import com.android.tools.build.bundletool.device.ApkMatcher; -import com.android.tools.build.bundletool.io.ApkSetBuilderFactory.ApkSetBuilder; import com.android.tools.build.bundletool.model.AndroidManifest; -import com.android.tools.build.bundletool.model.ApkListener; import com.android.tools.build.bundletool.model.ApkModifier; import com.android.tools.build.bundletool.model.ApkModifier.ApkDescription.ApkType; -import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.Bundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.GeneratedApks; @@ -64,21 +63,22 @@ import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.VariantKey; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; -import com.google.common.util.concurrent.ListeningExecutorService; import com.google.protobuf.Int32Value; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -87,99 +87,125 @@ /** Creates parts of table of contents and writes out APKs. */ public class ApkSerializerManager { - - private static final DateTimeFormatter DATE_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - private final AppBundle appBundle; - private final ApkListener apkListener; + private final Bundle bundle; private final ApkModifier apkModifier; - private final ListeningExecutorService executorService; + private final int firstVariantNumber; - private final boolean verbose; + private final ApkBuildMode apkBuildMode; private final ApkPathManager apkPathManager; private final ApkOptimizations apkOptimizations; + private final ApkSerializer apkSerializer; @Inject public ApkSerializerManager( - AppBundle appBundle, - Optional apkListener, + Bundle bundle, Optional apkModifier, - ListeningExecutorService executorService, @FirstVariantNumber Optional firstVariantNumber, @VerboseLogs boolean verbose, + ApkBuildMode apkBuildMode, ApkPathManager apkPathManager, - ApkOptimizations apkOptimizations) { - this.appBundle = appBundle; - this.apkListener = apkListener.orElse(ApkListener.NO_OP); + ApkOptimizations apkOptimizations, + ApkSerializer apkSerializer) { + this.bundle = bundle; this.apkModifier = apkModifier.orElse(ApkModifier.NO_OP); - this.executorService = executorService; this.firstVariantNumber = firstVariantNumber.orElse(0); - this.verbose = verbose; + this.apkBuildMode = apkBuildMode; this.apkPathManager = apkPathManager; this.apkOptimizations = apkOptimizations; + this.apkSerializer = apkSerializer; } - public void populateApkSetBuilder( - ApkSetBuilder apkSetBuilder, + /** Serialize App Bundle APKs. */ + public BuildApksResult serializeApkSet( + ApkSetWriter apkSetWriter, + GeneratedApks generatedApks, + GeneratedAssetSlices generatedAssetSlices, + Optional deviceSpec, + LocalTestingInfo localTestingInfo, + ImmutableSet permanentlyFusedModules) { + try { + BuildApksResult toc = + serializeApkSetContent( + apkSetWriter.getSplitsDirectory(), + generatedApks, + generatedAssetSlices, + deviceSpec, + localTestingInfo, + permanentlyFusedModules); + apkSetWriter.writeApkSet(toc); + return toc; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** Serialize SDK Bundle APKs. */ + public void serializeSdkApkSet(ApkSetWriter apkSetWriter, GeneratedApks generatedApks) { + try { + BuildSdkApksResult toc = + serializeSdkApkSetContent(apkSetWriter.getSplitsDirectory(), generatedApks); + apkSetWriter.writeApkSet(toc); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private BuildApksResult serializeApkSetContent( + Path outputDirectory, GeneratedApks generatedApks, GeneratedAssetSlices generatedAssetSlices, - ApkBuildMode apkBuildMode, Optional deviceSpec, LocalTestingInfo localTestingInfo, ImmutableSet permanentlyFusedModules) { ImmutableList allVariantsWithTargeting = - serializeApks(apkSetBuilder, generatedApks, apkBuildMode, deviceSpec); + serializeApks(outputDirectory, generatedApks, deviceSpec); ImmutableList allAssetSliceSets = - serializeAssetSlices(apkSetBuilder, generatedAssetSlices, apkBuildMode, deviceSpec); + serializeAssetSlices(outputDirectory, generatedAssetSlices, deviceSpec); // Finalize the output archive. BuildApksResult.Builder apksResult = BuildApksResult.newBuilder() - .setPackageName(appBundle.getPackageName()) + .setPackageName(bundle.getPackageName()) .addAllVariant(allVariantsWithTargeting) .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addAllAssetSliceSet(allAssetSliceSets) .setLocalTestingInfo(localTestingInfo); - if (appBundle.getBundleConfig().hasAssetModulesConfig()) { + if (bundle.getBundleConfig().hasAssetModulesConfig()) { apksResult.setAssetModulesInfo( - getAssetModulesInfo(appBundle.getBundleConfig().getAssetModulesConfig())); + getAssetModulesInfo(bundle.getBundleConfig().getAssetModulesConfig())); } - apksResult.addAllDefaultTargetingValue(getDefaultTargetingValues(appBundle.getBundleConfig())); + apksResult.addAllDefaultTargetingValue(getDefaultTargetingValues(bundle.getBundleConfig())); permanentlyFusedModules.forEach( moduleName -> apksResult.addPermanentlyFusedModules( PermanentlyFusedModule.newBuilder().setName(moduleName.getName()))); - apkSetBuilder.setTableOfContentsFile(apksResult.build()); - } - - @VisibleForTesting - ImmutableList serializeApksForDevice( - ApkSetBuilder apkSetBuilder, - GeneratedApks generatedApks, - DeviceSpec deviceSpec, - ApkBuildMode apkBuildMode) { - return serializeApks(apkSetBuilder, generatedApks, apkBuildMode, Optional.of(deviceSpec)); + return apksResult.build(); } - @VisibleForTesting - ImmutableList serializeApks(ApkSetBuilder apkSetBuilder, GeneratedApks generatedApks) { - return serializeApks(apkSetBuilder, generatedApks, ApkBuildMode.DEFAULT); + private BuildSdkApksResult serializeSdkApkSetContent( + Path outputDirectory, GeneratedApks generatedApks) { + ImmutableList allVariantsWithTargeting = + serializeApks(outputDirectory, generatedApks, /* deviceSpec= */ Optional.empty()); + SdkBundle sdkBundle = (SdkBundle) bundle; + return BuildSdkApksResult.newBuilder() + .setPackageName(sdkBundle.getPackageName()) + .addAllVariant(allVariantsWithTargeting) + .setBundletool( + Bundletool.newBuilder().setVersion(BundleToolVersion.getCurrentVersion().toString())) + .setVersion( + SdkVersionInformation.newBuilder() + .setVersionCode(sdkBundle.getVersionCode()) + .setMajor(Long.parseLong(sdkBundle.getMajorVersion())) + .setPatch(Long.parseLong(sdkBundle.getPatchVersion())) + .build()) + .build(); } @VisibleForTesting ImmutableList serializeApks( - ApkSetBuilder apkSetBuilder, GeneratedApks generatedApks, ApkBuildMode apkBuildMode) { - return serializeApks(apkSetBuilder, generatedApks, apkBuildMode, Optional.empty()); - } - - private ImmutableList serializeApks( - ApkSetBuilder apkSetBuilder, - GeneratedApks generatedApks, - ApkBuildMode apkBuildMode, - Optional deviceSpec) { + Path outputDirectory, GeneratedApks generatedApks, Optional deviceSpec) { validateInput(generatedApks, apkBuildMode); // Running with system APK mode generates a fused APK and additional unmatched language splits. @@ -202,11 +228,9 @@ private ImmutableList serializeApks( // 1. Remove APKs not matching the device spec. // 2. Modify the APKs based on the ApkModifier. // 3. Serialize all APKs in parallel. - ApkSerializer apkSerializer = new ApkSerializer(apkListener, apkBuildMode); // Modifies the APK using APK modifier, then returns a map by extracting the variant // of APK first and later clearing out its variant targeting. - ImmutableListMultimap finalSplitsByVariant = splitsByVariant.entries().stream() .filter(keyModuleSplitEntry -> deviceFilter.test(keyModuleSplitEntry.getValue())) @@ -220,19 +244,13 @@ private ImmutableList serializeApks( // After variant targeting of APKs are cleared, there might be duplicate APKs // which are removed and the distinct APKs are then serialized in parallel. - ImmutableMap apkDescriptionBySplit = + ImmutableBiMap splitsByRelativePath = finalSplitsByVariant.values().stream() .distinct() - .collect( - collectingAndThen( - toImmutableMap( - identity(), - (ModuleSplit split) -> { - ZipPath apkPath = apkPathManager.getApkPath(split); - return executorService.submit( - () -> apkSerializer.serialize(apkSetBuilder, split, apkPath)); - }), - ConcurrencyUtils::waitForAll)); + .collect(toImmutableBiMap(apkPathManager::getApkPath, identity())); + + ImmutableMap apkDescriptionsByRelativePath = + apkSerializer.serialize(outputDirectory, splitsByRelativePath); // Build the result proto. ImmutableList.Builder variants = ImmutableList.builder(); @@ -249,10 +267,11 @@ private ImmutableList serializeApks( for (BundleModuleName moduleName : splitsByModuleName.keySet()) { variant.addApkSet( ApkSet.newBuilder() - .setModuleMetadata(appBundle.getModule(moduleName).getModuleMetadata()) + .setModuleMetadata(bundle.getModule(moduleName).getModuleMetadata()) .addAllApkDescription( splitsByModuleName.get(moduleName).stream() - .map(apkDescriptionBySplit::get) + .map(split -> splitsByRelativePath.inverse().get(split)) + .map(apkDescriptionsByRelativePath::get) .collect(toImmutableList()))); } variants.add(variant.build()); @@ -263,9 +282,8 @@ private ImmutableList serializeApks( @VisibleForTesting ImmutableList serializeAssetSlices( - ApkSetBuilder apkSetBuilder, + Path outputDirectory, GeneratedAssetSlices generatedAssetSlices, - ApkBuildMode apkBuildMode, Optional deviceSpec) { Predicate deviceFilter = @@ -274,32 +292,27 @@ ImmutableList serializeAssetSlices( ::matchesModuleSplitByTargeting : alwaysTrue(); - ApkSerializer apkSerializer = new ApkSerializer(apkListener, apkBuildMode); - - ImmutableListMultimap generatedSlicesByModule = + ImmutableMap assetSplitsByRelativePath = generatedAssetSlices.getAssetSlices().stream() .filter(deviceFilter) + .collect(toImmutableMap(apkPathManager::getApkPath, identity())); + + ImmutableMap apkDescriptionsByRelativePath = + apkSerializer.serialize(outputDirectory, assetSplitsByRelativePath); + + ImmutableMap> serializedApksByModuleName = + assetSplitsByRelativePath.keySet().stream() .collect( groupingByDeterministic( - ModuleSplit::getModuleName, - mapping( - assetSlice -> { - ZipPath apkPath = apkPathManager.getApkPath(assetSlice); - return executorService.submit( - () -> apkSerializer.serialize(apkSetBuilder, assetSlice, apkPath)); - }, - toImmutableList()))) - .entrySet() - .stream() - .collect( - ImmutableListMultimap.flatteningToImmutableListMultimap( - Entry::getKey, entry -> waitForAll(entry.getValue()).stream())); - return generatedSlicesByModule.asMap().entrySet().stream() + relativePath -> assetSplitsByRelativePath.get(relativePath).getModuleName(), + mapping(apkDescriptionsByRelativePath::get, toImmutableList()))); + + return serializedApksByModuleName.entrySet().stream() .map( entry -> AssetSliceSet.newBuilder() .setAssetModuleMetadata( - getAssetModuleMetadata(appBundle.getModule(entry.getKey()))) + getAssetModuleMetadata(bundle.getModule(entry.getKey()))) .addAllApkDescription(entry.getValue()) .build()) .collect(toImmutableList()); @@ -346,7 +359,7 @@ private static void validateInput(GeneratedApks generatedApks, ApkBuildMode apkB checkArgument( generatedApks.getSplitApks().isEmpty() && generatedApks.getInstantApks().isEmpty() - && generatedApks.getHibernatedApks().isEmpty() + && generatedApks.getArchivedApks().isEmpty() && generatedApks.getSystemApks().isEmpty(), "Internal error: For universal APK expecting only standalone APKs."); break; @@ -354,7 +367,7 @@ private static void validateInput(GeneratedApks generatedApks, ApkBuildMode apkB checkArgument( generatedApks.getSplitApks().isEmpty() && generatedApks.getInstantApks().isEmpty() - && generatedApks.getHibernatedApks().isEmpty() + && generatedApks.getArchivedApks().isEmpty() && generatedApks.getStandaloneApks().isEmpty(), "Internal error: For system mode expecting only system APKs."); break; @@ -380,7 +393,7 @@ private static void validateInput(GeneratedApks generatedApks, ApkBuildMode apkB && generatedApks.getInstantApks().isEmpty() && generatedApks.getStandaloneApks().isEmpty() && generatedApks.getSystemApks().isEmpty(), - "Internal error: For hibernation mode expecting only hibernated APKs."); + "Internal error: For archive mode expecting only archived APKs."); break; } } @@ -471,60 +484,4 @@ private DeviceSpec addDefaultDeviceTierIfNecessary(DeviceSpec deviceSpec) { .orElse(0))) .build(); } - - private final class ApkSerializer { - private final ApkListener apkListener; - private final ApkBuildMode apkBuildMode; - - public ApkSerializer(ApkListener apkListener, ApkBuildMode apkBuildMode) { - this.apkListener = apkListener; - this.apkBuildMode = apkBuildMode; - } - - public ApkDescription serialize( - ApkSetBuilder apkSetBuilder, ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription; - switch (split.getSplitType()) { - case INSTANT: - apkDescription = apkSetBuilder.addInstantApk(split, apkPath); - break; - case SPLIT: - apkDescription = apkSetBuilder.addSplitApk(split, apkPath); - break; - case SYSTEM: - if (split.isBaseModuleSplit() && split.isMasterSplit()) { - apkDescription = apkSetBuilder.addSystemApk(split, apkPath); - } else { - apkDescription = apkSetBuilder.addSplitApk(split, apkPath); - } - break; - case STANDALONE: - apkDescription = - apkBuildMode.equals(ApkBuildMode.UNIVERSAL) - ? apkSetBuilder.addStandaloneUniversalApk(split) - : apkSetBuilder.addStandaloneApk(split, apkPath); - break; - case ASSET_SLICE: - apkDescription = apkSetBuilder.addAssetSliceApk(split, apkPath); - break; - case HIBERNATION: - apkDescription = apkSetBuilder.addHibernatedApk(split, apkPath); - break; - default: - throw new IllegalStateException("Unexpected splitType: " + split.getSplitType()); - } - - apkListener.onApkFinalized(apkDescription); - - if (verbose) { - System.out.printf( - "INFO: [%s] '%s' of type '%s' was written to disk.%n", - LocalDateTime.now(ZoneId.systemDefault()).format(DATE_FORMATTER), - apkPath, - split.getSplitType()); - } - - return apkDescription; - } - } } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerModule.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerModule.java index a8606d9a..a6f5c74b 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerModule.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerModule.java @@ -20,18 +20,18 @@ import dagger.Provides; import javax.inject.Provider; -/** Dagger module responsible for choosing the {@link ApkSerializerHelper}. */ +/** Dagger module responsible for choosing the {@link ApkSerializer}. */ @Module -public final class ApkSerializerModule { +public abstract class ApkSerializerModule { @Provides - static ApkSerializerHelper provideApkSerializerHelper( + static ApkSerializer provideApkSerializer( BuildApksCommand command, - Provider zipFlingerApkSerializerHelper, - Provider apkzlibApkSerializerHelper) { - return command.getEnableNewApkSerializer() - ? zipFlingerApkSerializerHelper.get() - : apkzlibApkSerializerHelper.get(); + Provider zipFlingerApkSerializerHelper, + Provider moduleSplitSerializerProvider) { + return command.getEnableApkSerializerWithoutBundleRecompression() + ? moduleSplitSerializerProvider.get() + : zipFlingerApkSerializerHelper.get(); } private ApkSerializerModule() {} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java deleted file mode 100644 index 3ccaa47d..00000000 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 - */ - -package com.android.tools.build.bundletool.io; - -import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_FILE; -import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; - -import com.android.bundle.Commands.ApkDescription; -import com.android.bundle.Commands.BuildApksResult; -import com.android.tools.build.bundletool.io.ZipBuilder.EntryOption; -import com.android.tools.build.bundletool.model.ModuleSplit; -import com.android.tools.build.bundletool.model.ZipPath; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Message; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** Factory for {@link ApkSetBuilder}. */ -public final class ApkSetBuilderFactory { - - /** Handles adding of {@link ModuleSplit} to the APK Set archive. */ - public interface ApkSetBuilder { - /** Adds a split APK to the APK Set archive. */ - ApkDescription addSplitApk(ModuleSplit split, ZipPath apkPath); - - /** Adds a standalone APK to the APK Set archive. */ - ApkDescription addStandaloneApk(ModuleSplit split, ZipPath apkPath); - - /** Adds a standalone universal APK to the APK Set archive. */ - ApkDescription addStandaloneUniversalApk(ModuleSplit split); - - /** Adds an instant split APK to the APK Set archive. */ - ApkDescription addInstantApk(ModuleSplit split, ZipPath apkPath); - - /** Adds an system APK to the APK Set archive. */ - ApkDescription addSystemApk(ModuleSplit split, ZipPath apkPath); - - /** Adds an asset slice APK to the APK Set archive. */ - ApkDescription addAssetSliceApk(ModuleSplit split, ZipPath apkPath); - - /** Adds a hibernated APK to the APK Set archive. */ - ApkDescription addHibernatedApk(ModuleSplit split, ZipPath apkPath); - - /** Sets the TOC file in the APK Set archive. */ - void setTableOfContentsFile(BuildApksResult tableOfContentsProto); - - /** Writes out the APK Set archive to the specified destination. */ - void writeTo(Path destinationPath); - } - - public static ApkSetBuilder createApkSetBuilder( - SplitApkSerializer splitApkSerializer, - StandaloneApkSerializer standaloneApkSerializer, - Path tempDir) { - return new ApkSetArchiveBuilder(splitApkSerializer, standaloneApkSerializer, tempDir); - } - - public static ApkSetBuilder createApkSetWithoutArchiveBuilder( - SplitApkSerializer splitApkSerializer, - StandaloneApkSerializer standaloneApkSerializer, - Path outputDir) { - return new ApkSetWithoutArchiveBuilder(splitApkSerializer, standaloneApkSerializer, outputDir); - } - - /** ApkSet builder that stores the generated APKs in the Apk Set archive. */ - public static class ApkSetArchiveBuilder implements ApkSetBuilder { - private final SplitApkSerializer splitApkSerializer; - private final StandaloneApkSerializer standaloneApkSerializer; - private final List relativeApkPaths; - - private final Path tempDirectory; - - private BuildApksResult tableOfContents; - - public ApkSetArchiveBuilder( - SplitApkSerializer splitApkSerializer, - StandaloneApkSerializer standaloneApkSerializer, - Path tempDirectory) { - this.splitApkSerializer = splitApkSerializer; - this.standaloneApkSerializer = standaloneApkSerializer; - this.tempDirectory = tempDirectory; - this.relativeApkPaths = Collections.synchronizedList(new ArrayList<>()); - } - - @Override - public ApkDescription addSplitApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - splitApkSerializer.writeSplitToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addInstantApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - splitApkSerializer.writeInstantSplitToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addAssetSliceApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - splitApkSerializer.writeAssetSliceToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addStandaloneApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - standaloneApkSerializer.writeToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addStandaloneUniversalApk(ModuleSplit split) { - ApkDescription apkDescription = - standaloneApkSerializer.writeToDiskAsUniversal(split, tempDirectory); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addSystemApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - standaloneApkSerializer.writeSystemApkToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public ApkDescription addHibernatedApk(ModuleSplit split, ZipPath apkPath) { - ApkDescription apkDescription = - standaloneApkSerializer.writeHibernatedApkToDisk(split, tempDirectory, apkPath); - relativeApkPaths.add(apkDescription.getPath()); - return apkDescription; - } - - @Override - public void setTableOfContentsFile(BuildApksResult tableOfContentsProto) { - tableOfContents = tableOfContentsProto; - } - - @Override - public void writeTo(Path destinationPath) { - ZipBuilder apkSetZipBuilder = new ZipBuilder(); - if (tableOfContents != null) { - apkSetZipBuilder.addFileWithProtoContent( - ZipPath.create(TABLE_OF_CONTENTS_FILE), tableOfContents); - } - // Sort APKs to make ordering deterministic. - for (String relativeApkPath : ImmutableList.sortedCopyOf(relativeApkPaths)) { - Path fullApkPath = tempDirectory.resolve(relativeApkPath); - checkFileExistsAndReadable(fullApkPath); - apkSetZipBuilder.addFileFromDisk( - ZipPath.create(relativeApkPath), fullApkPath.toFile(), EntryOption.UNCOMPRESSED); - } - try { - apkSetZipBuilder.writeTo(destinationPath); - } catch (IOException e) { - throw new UncheckedIOException( - String.format("Error while writing the APK Set archive to '%s'.", destinationPath), e); - } - } - } - - /** ApkSet builder that stores the generated APKs directly in the output directory. */ - public static class ApkSetWithoutArchiveBuilder implements ApkSetBuilder { - - private final SplitApkSerializer splitApkSerializer; - private final StandaloneApkSerializer standaloneApkSerializer; - private final Path outputDirectory; - - public ApkSetWithoutArchiveBuilder( - SplitApkSerializer splitApkSerializer, - StandaloneApkSerializer standaloneApkSerializer, - Path outputDirectory) { - this.outputDirectory = outputDirectory; - this.splitApkSerializer = splitApkSerializer; - this.standaloneApkSerializer = standaloneApkSerializer; - } - - @Override - public ApkDescription addInstantApk(ModuleSplit split, ZipPath apkPath) { - return splitApkSerializer.writeInstantSplitToDisk(split, outputDirectory, apkPath); - } - - @Override - public ApkDescription addSplitApk(ModuleSplit split, ZipPath apkPath) { - return splitApkSerializer.writeSplitToDisk(split, outputDirectory, apkPath); - } - - @Override - public ApkDescription addAssetSliceApk(ModuleSplit split, ZipPath apkPath) { - return splitApkSerializer.writeAssetSliceToDisk(split, outputDirectory, apkPath); - } - - @Override - public ApkDescription addStandaloneApk(ModuleSplit split, ZipPath apkPath) { - return standaloneApkSerializer.writeToDisk(split, outputDirectory, apkPath); - } - - @Override - public ApkDescription addStandaloneUniversalApk(ModuleSplit split) { - return standaloneApkSerializer.writeToDiskAsUniversal(split, outputDirectory); - } - - @Override - public ApkDescription addSystemApk(ModuleSplit split, ZipPath apkPath) { - return standaloneApkSerializer.writeSystemApkToDisk(split, outputDirectory, apkPath); - } - - @Override - public ApkDescription addHibernatedApk(ModuleSplit split, ZipPath apkPath) { - return standaloneApkSerializer.writeHibernatedApkToDisk(split, outputDirectory, apkPath); - } - - @Override - public void setTableOfContentsFile(BuildApksResult tableOfContentsProto) { - writeProtoFile(tableOfContentsProto, outputDirectory.resolve("toc.pb")); - } - - @Override - public void writeTo(Path destinationPath) { - // No-op. - } - - private void writeProtoFile(Message proto, Path outputFile) { - try (OutputStream outputStream = Files.newOutputStream(outputFile)) { - proto.writeTo(outputStream); - } catch (FileNotFoundException e) { - throw new UncheckedIOException("Can't create the output file: " + outputFile, e); - } catch (IOException e) { - throw new UncheckedIOException("Error while writing the output file: " + outputFile, e); - } - } - } -} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java new file mode 100644 index 00000000..5873dbe8 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_FILE; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Commands.BuildSdkApksResult; +import com.android.zipflinger.BytesSource; +import com.android.zipflinger.LargeFileSource; +import com.android.zipflinger.ZipArchive; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import java.util.zip.Deflater; + +/** Interface for ApkSet writer. */ +public interface ApkSetWriter { + + Path getSplitsDirectory(); + + void writeApkSet(BuildApksResult toc) throws IOException; + + void writeApkSet(BuildSdkApksResult toc) throws IOException; + + /** Creates ApkSet writer which stores all splits uncompressed inside output directory. */ + static ApkSetWriter directory(Path outputDirectory) { + return new ApkSetWriter() { + @Override + public Path getSplitsDirectory() { + return outputDirectory; + } + + @Override + public void writeApkSet(BuildApksResult toc) throws IOException { + Files.write(getSplitsDirectory().resolve(TABLE_OF_CONTENTS_FILE), toc.toByteArray()); + } + + @Override + public void writeApkSet(BuildSdkApksResult toc) throws IOException { + Files.write(getSplitsDirectory().resolve(TABLE_OF_CONTENTS_FILE), toc.toByteArray()); + } + }; + } + + /** Creates ApkSet writer which stores all splits as ZIP archive. */ + static ApkSetWriter zip(Path tempDirectory, Path outputFile) { + return new ApkSetWriter() { + @Override + public Path getSplitsDirectory() { + return tempDirectory; + } + + @Override + public void writeApkSet(BuildApksResult toc) throws IOException { + Stream apks = + toc.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()); + Stream assets = + toc.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()); + + ImmutableSet apkRelativePaths = + Stream.concat(apks, assets) + .map(ApkDescription::getPath) + .sorted() + .collect(toImmutableSet()); + + zipApkSet(apkRelativePaths, toc.toByteArray()); + } + + @Override + public void writeApkSet(BuildSdkApksResult toc) throws IOException { + Stream apks = + toc.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()); + + ImmutableSet apkRelativePaths = + apks.map(ApkDescription::getPath).sorted().collect(toImmutableSet()); + + zipApkSet(apkRelativePaths, toc.toByteArray()); + } + + private void zipApkSet(ImmutableSet apkRelativePaths, byte[] tocBytes) + throws IOException { + try (ZipArchive zipArchive = new ZipArchive(outputFile)) { + zipArchive.add( + new BytesSource(tocBytes, TABLE_OF_CONTENTS_FILE, Deflater.NO_COMPRESSION)); + + for (String relativePath : apkRelativePaths) { + zipArchive.add( + new LargeFileSource( + getSplitsDirectory().resolve(relativePath), + /* tmpStorage= */ null, + relativePath, + Deflater.NO_COMPRESSION)); + } + } + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkzlibApkSerializerHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ApkzlibApkSerializerHelper.java deleted file mode 100644 index cbc18e26..00000000 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkzlibApkSerializerHelper.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 - */ -package com.android.tools.build.bundletool.io; - -import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; -import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; -import static com.android.tools.build.bundletool.model.utils.files.FileUtils.createParentDirectories; -import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NO_DEFAULT_UNCOMPRESS_EXTENSIONS; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.ImmutableList.toImmutableList; - -import com.android.bundle.Config.BundleConfig; -import com.android.bundle.Config.ResourceOptimizations.SparseEncoding; -import com.android.tools.build.apkzlib.zfile.ZFiles; -import com.android.tools.build.apkzlib.zip.AlignmentRule; -import com.android.tools.build.apkzlib.zip.AlignmentRules; -import com.android.tools.build.apkzlib.zip.ZFile; -import com.android.tools.build.apkzlib.zip.ZFileOptions; -import com.android.tools.build.bundletool.androidtools.Aapt2Command; -import com.android.tools.build.bundletool.io.ZipBuilder.EntryOption; -import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; -import com.android.tools.build.bundletool.model.ModuleEntry; -import com.android.tools.build.bundletool.model.ModuleSplit; -import com.android.tools.build.bundletool.model.ZipPath; -import com.android.tools.build.bundletool.model.utils.PathMatcher; -import com.android.tools.build.bundletool.model.utils.files.FileUtils; -import com.android.tools.build.bundletool.model.version.Version; -import com.google.common.base.Optional; -import com.google.common.base.Predicates; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.regex.Pattern; -import javax.inject.Inject; - -/** Serializes APKs to Proto or Binary format. */ -final class ApkzlibApkSerializerHelper extends ApkSerializerHelper { - - /** Suffix for native libraries. */ - private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; - - private static final Pattern NATIVE_LIBRARIES_PATTERN = Pattern.compile("lib/[^/]+/[^/]+\\.so"); - - /** - * Alignment rule for all APKs. - * - *

    - *
  • Align by 4 all uncompressed files. - *
  • Align by 4096 all uncompressed native libraries. - *
- * - * Note that it's fine to always provide the same alignment rule regardless of the value of - * 'extractNativeLibs' because apkzlib will only apply these rules to uncompressed files, so a - * compressed file will remain unaligned. - */ - static final AlignmentRule APK_ALIGNMENT_RULE = - AlignmentRules.compose( - AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096), - AlignmentRules.constant(4)); - - private final Aapt2Command aapt2Command; - private final Version bundletoolVersion; - private final ImmutableList uncompressedPathMatchers; - private final ApkSigner apkSigner; - private final boolean enableSparseEncoding; - - @Inject - ApkzlibApkSerializerHelper( - Aapt2Command aapt2Command, - Version bundletoolVersion, - BundleConfig bundleConfig, - ApkSigner apkSigner) { - this.aapt2Command = aapt2Command; - this.bundletoolVersion = bundletoolVersion; - this.uncompressedPathMatchers = - bundleConfig.getCompression().getUncompressedGlobList().stream() - .map(PathMatcher::createFromGlob) - .collect(toImmutableList()); - this.apkSigner = apkSigner; - this.enableSparseEncoding = - bundleConfig - .getOptimizations() - .getResourceOptimizations() - .getSparseEncoding() - .equals(SparseEncoding.ENFORCED); - } - - @Override - public Path writeToZipFile(ModuleSplit split, Path outputPath) { - try (TempDirectory tempDirectory = new TempDirectory()) { - writeToZipFile(split, outputPath, tempDirectory); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return outputPath; - } - - private void writeToZipFile(ModuleSplit split, Path outputPath, TempDirectory tempDir) - throws IOException { - checkFileDoesNotExist(outputPath); - createParentDirectories(outputPath); - - // Sign the embedded APKs - split = apkSigner.signEmbeddedApks(split); - - // Write a Proto-APK with only files that aapt2 requires as part of the convert command. - Path partialProtoApk = tempDir.getPath().resolve("proto.apk"); - writeProtoApk(split, partialProtoApk); - - // Have aapt2 convert the Proto-APK to a Binary-APK. - Path binaryApk = tempDir.getPath().resolve("binary.apk"); - - if (enableSparseEncoding && split.getResourceTable().isPresent()) { - Path interimApk = tempDir.getPath().resolve("interim.apk"); - aapt2Command.convertApkProtoToBinary(partialProtoApk, interimApk); - aapt2Command.optimizeToSparseResourceTables(interimApk, binaryApk); - } else { - aapt2Command.convertApkProtoToBinary(partialProtoApk, binaryApk); - } - checkState(Files.exists(binaryApk), "No APK created by aapt2 convert command."); - - // Create a new APK that includes files processed by aapt2 and the other ones. - try (ZFile zOutputApk = - ZFiles.apk( - outputPath.toFile(), - createZFileOptions(tempDir.getPath()) - .setAlignmentRule(APK_ALIGNMENT_RULE) - .setCoverEmptySpaceUsingExtraField(true) - // Clear timestamps on zip entries to minimize diffs between APKs. - .setNoTimestamps(true), - /* signingOptions= */ Optional.absent(), - /* builtBy= */ null, - /* createdBy= */ null, - // Use ZFileOptions.setAlwaysGenerateJarManifest(false) when this is released. - /* writeManifest= */ false); - ZFile zAapt2Files = - ZFile.openReadOnly(binaryApk.toFile(), createZFileOptions(tempDir.getPath()))) { - - // Add files from the binary APK generated by AAPT2. - zOutputApk.mergeFrom(zAapt2Files, /* ignoreFilter= */ Predicates.alwaysFalse()); - - // Add the remaining files. - addNonAapt2Files(zOutputApk, split); - zOutputApk.sortZipContents(); - } - - apkSigner.signApk(outputPath, split); - } - - /** - * Creates a proto-APK from the {@link ModuleSplit} and stores it on disk. - * - *

Note that it only includes files that aapt2 transforms, i.e. AndroidManifest.xml, resource - * table, and resources. This is an optimization to prevent aapt2 from copying unnecessary files. - * - * @param split In-memory representation of the APK to write. - * @param outputPath Path to where the APK should be created. - * @return The path to the created APK. - */ - private Path writeProtoApk(ModuleSplit split, Path outputPath) throws IOException { - boolean extractNativeLibs = split.getAndroidManifest().getExtractNativeLibsValue().orElse(true); - - ZipBuilder zipBuilder = new ZipBuilder(); - for (ModuleEntry entry : split.getEntries()) { - ZipPath pathInApk = toApkEntryPath(entry.getPath()); - if (!requiresAapt2Conversion(pathInApk)) { - continue; - } - - EntryOption[] entryOptions = - entryOptionForPath( - pathInApk, - /* uncompressNativeLibs= */ !extractNativeLibs, - /* forceUncompressed= */ entry.getForceUncompressed()); - zipBuilder.addFile(pathInApk, entry.getContent(), entryOptions); - } - - split - .getResourceTable() - .ifPresent( - resourceTable -> - zipBuilder.addFileWithProtoContent( - SpecialModuleEntry.RESOURCE_TABLE.getPath(), resourceTable)); - zipBuilder.addFileWithProtoContent( - ZipPath.create(MANIFEST_FILENAME), split.getAndroidManifest().getManifestRoot().getProto()); - - zipBuilder.writeTo(outputPath); - - return outputPath; - } - - private EntryOption[] entryOptionForPath( - ZipPath path, boolean uncompressNativeLibs, boolean forceUncompressed) { - if (mayCompress(path, uncompressNativeLibs, forceUncompressed)) { - return new EntryOption[] {}; - } else { - return new EntryOption[] {EntryOption.UNCOMPRESSED}; - } - } - - /** - * Returns true if the specified file may be compressed in the final generated APK. - * - *

If this method returns true, the preference is that the file is compressed within the APK, - * however this isn't guaranteed, e.g. if the file's compressed size is greater than the - * uncompressed size. If this method returns false, the file must be stored uncompressed. - */ - private boolean mayCompress( - ZipPath path, boolean uncompressNativeLibs, boolean forceUncompressed) { - if (uncompressedPathMatchers.stream() - .anyMatch(pathMatcher -> pathMatcher.matches(path.toString()))) { - return false; - } - - if (forceUncompressed) { - return false; - } - - // Common extensions that should remain uncompressed because compression doesn't provide any - // gains. - if (!NO_DEFAULT_UNCOMPRESS_EXTENSIONS.enabledForVersion(bundletoolVersion) - && NO_COMPRESSION_EXTENSIONS.contains(FileUtils.getFileExtension(path))) { - return false; - } - - // Uncompressed native libraries (supported since SDK 23 - Android M). - if (uncompressNativeLibs && NATIVE_LIBRARIES_PATTERN.matcher(path.toString()).matches()) { - return false; - } - - // By default, may be compressed. - return true; - } - - /** Takes the given APK and adds the files that weren't processed by AAPT2. */ - private void addNonAapt2Files(ZFile zFile, ModuleSplit split) throws IOException { - boolean extractNativeLibs = split.getAndroidManifest().getExtractNativeLibsValue().orElse(true); - - // Add the non-Aapt2 files. - for (ModuleEntry entry : split.getEntries()) { - ZipPath pathInApk = toApkEntryPath(entry.getPath()); - if (!requiresAapt2Conversion(pathInApk)) { - boolean mayCompress = - mayCompress(pathInApk, !extractNativeLibs, entry.getForceUncompressed()); - addFile(zFile, pathInApk, entry, mayCompress); - } - } - } - - void addFile(ZFile zFile, ZipPath pathInApk, ModuleEntry entry, boolean mayCompress) - throws IOException { - try (InputStream entryInputStream = entry.getContent().openStream()) { - zFile.add(pathInApk.toString(), entryInputStream, mayCompress); - } - } - - private static ZFileOptions createZFileOptions(Path tempDir) { - ZFileOptions options = new ZFileOptions(); - return options; - } -} diff --git a/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java index a240e9b8..96b3b3d1 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java @@ -110,6 +110,14 @@ public void writeToDisk(AppBundle bundle, Path pathOnDisk) throws IOException { moduleDir.resolve(SpecialModuleEntry.APEX_TABLE.getPath()), apexConfig, compression)); + module + .getRuntimeEnabledSdkConfig() + .ifPresent( + runtimeEnabledSdkConfig -> + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(SpecialModuleEntry.RUNTIME_ENABLED_SDK_CONFIG.getPath()), + runtimeEnabledSdkConfig, + compression)); } zipBuilder.writeTo(pathOnDisk); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPack.java b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPack.java new file mode 100644 index 00000000..629972c6 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPack.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.zipflinger.Entry; +import com.android.zipflinger.ZipArchive; +import com.android.zipflinger.ZipMap; +import com.android.zipflinger.ZipSource; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.collect.Streams; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.ToLongFunction; +import java.util.stream.Collectors; + +/** + * Set of module entries whose content is stored inside zip archive. + * + *

Inside zip archive module entries are stored with names that were assigned by {@link + * ModuleEntriesPacker}. + */ +class ModuleEntriesPack { + + /** + * Prefixes of entry names that are in use by this pack. + * + *

Each entry name in zip archive has the following format: {prefix}{generatedPart}. + * + *

Two packs are mergeable if they use different name prefixes. + */ + private final ImmutableSet namePrefixes; + + /** Zip archive that stores content of module entries. */ + private final ZipMap zipMap; + + /** + * Map that stores mappings between ModuleEntry and zip entry name. + * + *

This map should be read-only in this class, it uses mutable IdentityHashMap because there is + * no immutable analogue for it. + */ + private final IdentityHashMap entryNameByModuleEntry; + + ModuleEntriesPack( + ImmutableSet namePrefixes, + ZipMap zipMap, + IdentityHashMap entryNameByModuleEntry) { + this.namePrefixes = namePrefixes; + this.zipMap = zipMap; + this.entryNameByModuleEntry = entryNameByModuleEntry; + } + + Entry getZipEntry(ModuleEntry entry) { + checkArgument( + entryNameByModuleEntry.containsKey(entry), + "Module entry %s is not available in pack", + entry); + return zipMap.getEntries().get(entryNameByModuleEntry.get(entry)); + } + + boolean hasEntry(ModuleEntry entry) { + return entryNameByModuleEntry.containsKey(entry); + } + + /** + * Selects module entries as a {@link ZipSource} which next can be added into a new {@link + * ZipArchive}. + * + *

Requires to provide {@code nameFunction} which assigns final names for each {@link + * ModuleEntry}. + */ + ZipSource select( + ImmutableList moduleEntries, Function nameFunction) { + return select(moduleEntries, nameFunction, /* alignmentFunction= */ entry -> 0L); + } + + /** + * Selects module entries as a {@link ZipSource} which next can be added into a new {@link + * ZipArchive}. + * + *

Requires to provide {@code nameFunction} which assigns final names for each {@link + * ModuleEntry} and {@code alignmentFunction} which assigns alignment in the final {@link + * ZipSource}. + */ + ZipSource select( + ImmutableList moduleEntries, + Function nameFunction, + ToLongFunction alignmentFunction) { + ZipSource source = new ZipSource(zipMap); + for (ModuleEntry entry : moduleEntries) { + checkArgument( + entryNameByModuleEntry.containsKey(entry), + "Module entry %s is not available in the pack.", + entry); + source.select( + entryNameByModuleEntry.get(entry), + nameFunction.apply(entry), + ZipSource.COMPRESSION_NO_CHANGE, + alignmentFunction.applyAsLong(entry)); + } + return source; + } + + /** + * Merges two packs into a single one. + * + *

Prefers to merge smaller pack into bigger one. Removes zip file of unused pack. + */ + public ModuleEntriesPack mergeWith(ModuleEntriesPack anotherPack) { + checkArgument( + Collections.disjoint(namePrefixes, anotherPack.namePrefixes), + "Both packs contain the same name prefix."); + + try { + long thisSize = Files.size(zipMap.getPath()); + long anotherSize = Files.size(anotherPack.zipMap.getPath()); + ModuleEntriesPack to = thisSize > anotherSize ? this : anotherPack; + ModuleEntriesPack from = thisSize > anotherSize ? anotherPack : this; + try (ZipArchive archive = new ZipArchive(to.zipMap.getPath())) { + archive.add(ZipSource.selectAll(from.zipMap.getPath())); + } + Files.delete(from.zipMap.getPath()); + IdentityHashMap mergedNames = + Streams.concat( + entryNameByModuleEntry.entrySet().stream(), + anotherPack.entryNameByModuleEntry.entrySet().stream()) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (a, b) -> b, + Maps::newIdentityHashMap)); + return new ModuleEntriesPack( + Sets.union(to.namePrefixes, from.namePrefixes).immutableCopy(), + ZipMap.from(to.zipMap.getPath()), + mergedNames); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java new file mode 100644 index 00000000..9e7dd177 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.zipflinger.ZipMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.io.ByteSource; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.IdentityHashMap; +import java.util.Map; + +/** Class to build {@link ModuleEntriesPack}. */ +class ModuleEntriesPacker { + private final String namePrefix; + private final Path outputZip; + private final IdentityHashMap entryNameByModuleEntry; + private final Map contentByEntryName; + private final Map assignedEntryNameByBundleLocation; + + private final NameAssigner nameAssigner; + + public ModuleEntriesPacker(Path outputZip, String namePrefix) { + this.namePrefix = namePrefix; + this.outputZip = outputZip; + nameAssigner = new NameAssigner(namePrefix); + entryNameByModuleEntry = Maps.newIdentityHashMap(); + contentByEntryName = Maps.newHashMap(); + assignedEntryNameByBundleLocation = Maps.newHashMap(); + } + + /** + * Adds {@link ModuleEntry} to the pack. + * + *

At this point entry is assigned a name and is added into a list of entries to be packed. + */ + ModuleEntriesPacker add(ModuleEntry entry) { + if (entryNameByModuleEntry.containsKey(entry)) { + return this; + } + String entryName = + entry.getBundleLocation().isPresent() + ? assignedByBundleLocation(entry) + : nameAssigner.nextName(); + entryNameByModuleEntry.put(entry, entryName); + contentByEntryName.putIfAbsent(entryName, entry.getContent()); + return this; + } + + /** + * Packs all entries which were previously added with provided {@link Zipper} and returns {@link + * ModuleEntriesPack} pointing to this pack. + */ + public ModuleEntriesPack pack(Zipper zipper) { + try { + zipper.zip(outputZip, ImmutableMap.copyOf(contentByEntryName)); + + IdentityHashMap copyOfEntryNameByModuleEntry = Maps.newIdentityHashMap(); + copyOfEntryNameByModuleEntry.putAll(entryNameByModuleEntry); + return new ModuleEntriesPack( + ImmutableSet.of(namePrefix), ZipMap.from(outputZip), copyOfEntryNameByModuleEntry); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private String assignedByBundleLocation(ModuleEntry entry) { + return assignedEntryNameByBundleLocation.computeIfAbsent( + entry.getBundleLocation().get(), (location) -> nameAssigner.nextName()); + } + + /** Helper class that allows to generate zip entry names. */ + private static class NameAssigner { + private final String prefix; + + private int counter = 0; + + NameAssigner(String prefix) { + this.prefix = prefix; + } + + String nextName() { + return prefix + String.valueOf(counter++); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java new file mode 100644 index 00000000..464dd450 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import static com.android.tools.build.bundletool.io.ApkSerializerHelper.requiresAapt2Conversion; +import static com.android.tools.build.bundletool.io.ApkSerializerHelper.toApkEntryPath; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NO_DEFAULT_UNCOMPRESS_EXTENSIONS; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.function.Function.identity; + +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.commands.BuildApksModule.VerboseLogs; +import com.android.tools.build.bundletool.model.ApkListener; +import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.utils.PathMatcher; +import com.android.tools.build.bundletool.model.utils.files.FileUtils; +import com.android.tools.build.bundletool.model.version.Version; +import com.android.zipflinger.Entry; +import com.android.zipflinger.ZipArchive; +import com.android.zipflinger.ZipSource; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Streams; +import com.google.common.io.ByteSource; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Comparator; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.zip.Deflater; +import javax.inject.Inject; + +/** Serializes module splits on disk. */ +public class ModuleSplitSerializer extends ApkSerializer { + /** Suffix for native libraries. */ + private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; + + private static final Pattern NATIVE_LIBRARIES_PATTERN = Pattern.compile("lib/[^/]+/[^/]+\\.so"); + + private final Aapt2ResourceConverter aapt2ResourceConverter; + private final ApkSigner apkSigner; + private final ImmutableList uncompressedPathMatchers; + private final Version bundletoolVersion; + private final ListeningExecutorService executorService; + + @Inject + ModuleSplitSerializer( + Optional apkListener, + @VerboseLogs boolean verbose, + Aapt2ResourceConverter aapt2ResourceConverterFactory, + ApkSigner apkSigner, + BundleConfig bundleConfig, + Version bundletoolVersion, + ListeningExecutorService executorService) { + super(apkListener, verbose); + this.aapt2ResourceConverter = aapt2ResourceConverterFactory; + this.apkSigner = apkSigner; + this.uncompressedPathMatchers = + bundleConfig.getCompression().getUncompressedGlobList().stream() + .map(PathMatcher::createFromGlob) + .collect(toImmutableList()); + this.bundletoolVersion = bundletoolVersion; + this.executorService = executorService; + } + + /** + * Serializes module splits on disk under {@code outputDirectory}. + * + *

Returns {@link ApkDescription} for each serialized split keyed by relative path of module + * split. + */ + public ImmutableMap serialize( + Path outputDirectory, ImmutableMap splitsByRelativePath) { + // Prepare original splits by: + // * signing embedded APKs + // * injecting manifest and resource table as module entries. + ImmutableList preparedSplits = + splitsByRelativePath.values().stream() + .map(apkSigner::signEmbeddedApks) + .map(ModuleSplitSerializer::injectManifestAndResourceTableAsEntries) + .collect(toImmutableList()); + + try (SerializationFilesManager filesManager = new SerializationFilesManager()) { + // Convert module splits to binary format and apply uncompressed globs specified in + // BundleConfig. We do it in this order because as specified in documentation the matching + // for uncompressed globs is done against paths in final APKs. + ImmutableList binarySplits = + aapt2ResourceConverter.convert(preparedSplits, filesManager).stream() + .map(this::applyUncompressedGlobsAndUncompressedNativeLibraries) + .collect(toImmutableList()); + + // Build a pack from entries which may be compressed inside final APKs. 'May be + // compressed' means that for these entries we will decide later should they be compressed + // or not based on whether we gain enough savings from compression. + ModuleEntriesPack maybeCompressedEntriesPack = + buildCompressedEntriesPack( + filesManager.getCompressedResourceEntriesPackPath(), + filesManager.getCompressedEntriesPackPath(), + binarySplits); + + // Build a pack with entries that are uncompressed in final APKs: force uncompressed entries + // + entries that have very low compression ratio. + ModuleEntriesPack uncompressedEntriesPack = + buildUncompressedEntriesPack( + filesManager.getUncompressedEntriesPackPath(), + binarySplits, + maybeCompressedEntriesPack); + + // Now content of all binary apks is already moved to compressed/uncompressed packs. Delete + // them to free space. + filesManager.closeAndRemoveBinaryApks(); + + // Merge two packs together, so we have all entries for final APKs inside one pack. If the + // same entry is in both packs we prefer uncompressed one, because it means this entry + // has very low compression ratio, it makes no sense to put it in compressed form. + ModuleEntriesPack allEntriesPack = + maybeCompressedEntriesPack.mergeWith(uncompressedEntriesPack); + + // Serialize and sign final APKs. + ImmutableList> apkDescriptions = + Streams.zip( + splitsByRelativePath.keySet().stream(), + binarySplits.stream(), + (relativePath, split) -> + executorService.submit( + () -> + serializeAndSignSplit( + outputDirectory, + relativePath, + split, + allEntriesPack, + uncompressedEntriesPack))) + .collect(toImmutableList()); + + return ConcurrencyUtils.waitForAll(apkDescriptions).stream() + .collect(toImmutableMap(apk -> ZipPath.create(apk.getPath()), identity())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Builds pack with compressed entries, resource entries are compressed with the best compression + * level (9) and all others with default compression level (6). + */ + private ModuleEntriesPack buildCompressedEntriesPack( + Path resourcesOutputPath, Path compressedOutputPath, Collection splits) { + ModuleEntriesPacker otherEntriesPacker = + new ModuleEntriesPacker(compressedOutputPath, /* namePrefix= */ "c_"); + ModuleEntriesPacker resourceEntriesPacker = + new ModuleEntriesPacker(resourcesOutputPath, /* namePrefix= */ "r_"); + splits.stream() + .flatMap(split -> split.getEntries().stream()) + .filter(entry -> !entry.getForceUncompressed()) + .forEach( + entry -> { + if (requiresAapt2Conversion(toApkEntryPath(entry.getPath()))) { + resourceEntriesPacker.add(entry); + } else { + otherEntriesPacker.add(entry); + } + }); + + ModuleEntriesPack resourceEntriesPack = + resourceEntriesPacker.pack( + Zipper.compressedZip(executorService, Deflater.BEST_COMPRESSION)); + ModuleEntriesPack otherEntriesPack = + otherEntriesPacker.pack( + Zipper.compressedZip(executorService, Deflater.DEFAULT_COMPRESSION)); + + return resourceEntriesPack.mergeWith(otherEntriesPack); + } + + private ModuleEntriesPack buildUncompressedEntriesPack( + Path outputPath, Collection splits, ModuleEntriesPack compressedPack) { + ModuleEntriesPacker entriesPacker = new ModuleEntriesPacker(outputPath, /* namePrefix= */ "u_"); + splits.stream() + .flatMap(split -> split.getEntries().stream()) + .filter( + entry -> + entry.getForceUncompressed() + || shouldUncompressBecauseOfLowRatio(entry, compressedPack)) + .forEach(entriesPacker::add); + return entriesPacker.pack(Zipper.uncompressedZip()); + } + + private ApkDescription serializeAndSignSplit( + Path outputDirectory, + ZipPath apkRelativePath, + ModuleSplit split, + ModuleEntriesPack allEntriesPack, + ModuleEntriesPack uncompressedEntriesPack) { + Path outputPath = outputDirectory.resolve(apkRelativePath.toString()); + ApkDescription apkDescription = + ApkDescriptionHelper.createApkDescription(apkRelativePath, split); + + serializeSplit(outputPath, split, allEntriesPack, uncompressedEntriesPack); + apkSigner.signApk(outputPath, split); + notifyApkSerialized(apkDescription, split.getSplitType()); + + return apkDescription; + } + + private void serializeSplit( + Path outputPath, + ModuleSplit split, + ModuleEntriesPack allEntriesPack, + ModuleEntriesPack uncompressedEntriesPack) { + FileUtils.createDirectories(outputPath.getParent()); + try (ZipArchive archive = new ZipArchive(outputPath)) { + ImmutableMap moduleEntriesByName = + split.getEntries().stream() + .collect( + toImmutableMap( + entry -> toApkEntryPath(entry.getPath()), + entry -> entry, + // If two entries end up at the same path in the APK, pick one arbitrarily. + // e.g. base/assets/foo and base/root/assets/foo. + (a, b) -> b)); + + // Sorting entries by name for determinism. + ImmutableList sortedEntries = + ImmutableList.sortedCopyOf( + Comparator.comparing(e -> toApkEntryPath(e.getPath())), moduleEntriesByName.values()); + + ZipSource zipSource = + allEntriesPack.select( + sortedEntries, + entry -> toApkEntryPath(entry.getPath(), /* binaryApk= */ true).toString(), + entry -> alignmentForEntry(entry, uncompressedEntriesPack)); + archive.add(zipSource); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Returns alignment for {@link ModuleEntry} inside APK. + * + *

Uncompressed native libraries inside APK must be aligned by 4096 and all other uncompressed + * entries aligned by 4 bytes. + */ + private static long alignmentForEntry( + ModuleEntry entry, ModuleEntriesPack uncompressedEntriesPack) { + if (!uncompressedEntriesPack.hasEntry(entry)) { + return 0; + } + return entry.getPath().toString().endsWith(NATIVE_LIBRARIES_SUFFIX) ? 4096 : 4; + } + + /** + * Injects Android manifest and resource table which have special fields {@code + * ModuleSplit.getAndroidManifest} and {@code ModuleSplit.getResourceTable} as regular module + * entries into {@link ModuleSplit}. + */ + private static ModuleSplit injectManifestAndResourceTableAsEntries(ModuleSplit split) { + ImmutableList.Builder splitEntries = ImmutableList.builder(); + + splitEntries.add( + ModuleEntry.builder() + .setForceUncompressed(false) + .setContent( + ByteSource.wrap( + split.getAndroidManifest().getManifestRoot().getProto().toByteArray())) + .setPath(SpecialModuleEntry.ANDROID_MANIFEST.getPath()) + .build()); + + if (split.getResourceTable().isPresent()) { + splitEntries.add( + ModuleEntry.builder() + .setForceUncompressed(true) + .setContent(ByteSource.wrap(split.getResourceTable().get().toByteArray())) + .setPath(SpecialModuleEntry.RESOURCE_TABLE.getPath()) + .build()); + } + + split.getEntries().stream() + .filter(entry -> !SpecialModuleEntry.ANDROID_MANIFEST.getPath().equals(entry.getPath())) + .filter(entry -> !SpecialModuleEntry.RESOURCE_TABLE.getPath().equals(entry.getPath())) + .forEach(splitEntries::add); + return split.toBuilder().setEntries(splitEntries.build()).build(); + } + + private ModuleSplit applyUncompressedGlobsAndUncompressedNativeLibraries(ModuleSplit split) { + boolean uncompressNativeLibs = + !split.getAndroidManifest().getExtractNativeLibsValue().orElse(true); + return split.toBuilder() + .setEntries( + split.getEntries().stream() + .map( + entry -> + shouldUncompressEntry(entry, uncompressNativeLibs) + ? entry.toBuilder().setForceUncompressed(true).build() + : entry) + .collect(toImmutableList())) + .build(); + } + + private boolean shouldUncompressEntry(ModuleEntry entry, boolean uncompressNativeLibs) { + // If entry is already uncompressed no need to match it. + if (entry.getForceUncompressed()) { + return false; + } + return matchesForceUncompressedPath(entry) + || matchesUncompressedNativeLib(entry, uncompressNativeLibs); + } + + /** Whether entry is native library that should be uncompressed, . */ + private boolean matchesUncompressedNativeLib(ModuleEntry entry, boolean uncompressNativeLibs) { + return uncompressNativeLibs + && NATIVE_LIBRARIES_PATTERN.matcher(entry.getPath().toString()).matches(); + } + + /** Whether entry path in APK matches uncompressed globs specified in BundleConfig. */ + private boolean matchesForceUncompressedPath(ModuleEntry entry) { + // Android manifest is always compressed. + if (entry.getPath().equals(SpecialModuleEntry.ANDROID_MANIFEST.getPath())) { + return false; + } + + String path = toApkEntryPath(entry.getPath()).toString(); + if (uncompressedPathMatchers.stream().anyMatch(pathMatcher -> pathMatcher.matches(path))) { + return true; + } + // Common extensions that should remain uncompressed because compression doesn't provide any + // gains. + if (!NO_DEFAULT_UNCOMPRESS_EXTENSIONS.enabledForVersion(bundletoolVersion) + && ApkSerializerHelper.NO_COMPRESSION_EXTENSIONS.contains( + FileUtils.getFileExtension(ZipPath.create(path)))) { + return true; + } + return false; + } + + /** + * Whether module entry should be put in uncompressed form because savings for the entry is low. + */ + private boolean shouldUncompressBecauseOfLowRatio( + ModuleEntry moduleEntry, ModuleEntriesPack compressedPack) { + Entry entry = compressedPack.getZipEntry(moduleEntry); + long compressedSize = entry.getCompressedSize(); + long uncompressedSize = entry.getUncompressedSize(); + + // Copying logic from aapt2: require at least 10% gains in savings. + if (moduleEntry.getPath().startsWith("res")) { + return compressedSize + compressedSize / 10 > uncompressedSize; + } + return compressedSize >= uncompressedSize; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/SdkBundleSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/SdkBundleSerializer.java new file mode 100644 index 00000000..ef400286 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/SdkBundleSerializer.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.io; + +import static com.android.tools.build.bundletool.model.AppBundle.BUNDLE_CONFIG_FILE_NAME; +import static com.android.tools.build.bundletool.model.AppBundle.METADATA_DIRECTORY; + +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.ZipPath; +import com.google.common.io.ByteSource; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map.Entry; + +/** Serializer of {@link SdkBundle} instances onto disk. */ +public class SdkBundleSerializer { + + /** Writes the SDK Bundle on disk at the given location. */ + public void writeToDisk(SdkBundle bundle, Path pathOnDisk) throws IOException { + ZipBuilder zipBuilder = new ZipBuilder(); + + zipBuilder.addFileWithProtoContent( + ZipPath.create(BUNDLE_CONFIG_FILE_NAME), bundle.getBundleConfig()); + + // BUNDLE-METADATA + for (Entry metadataEntry : + bundle.getBundleMetadata().getFileContentMap().entrySet()) { + zipBuilder.addFile( + METADATA_DIRECTORY.resolve(metadataEntry.getKey()), metadataEntry.getValue()); + } + + // Base module (the only module in an ASB) + BundleModule module = bundle.getModule(); + ZipPath moduleDir = ZipPath.create(module.getName().toString()); + + for (ModuleEntry entry : module.getEntries()) { + ZipPath entryPath = moduleDir.resolve(entry.getPath()); + zipBuilder.addFile(entryPath, entry.getContent()); + } + + // Special module files are not represented as module entries (above). + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(SpecialModuleEntry.ANDROID_MANIFEST.getPath()), + module.getAndroidManifest().getManifestRoot().getProto()); + module + .getAssetsConfig() + .ifPresent( + assetsConfig -> + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(SpecialModuleEntry.ASSETS_TABLE.getPath()), assetsConfig)); + module + .getNativeConfig() + .ifPresent( + nativeConfig -> + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(SpecialModuleEntry.NATIVE_LIBS_TABLE.getPath()), + nativeConfig)); + module + .getResourceTable() + .ifPresent( + resourceTable -> + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(SpecialModuleEntry.RESOURCE_TABLE.getPath()), resourceTable)); + + zipBuilder.writeTo(pathOnDisk); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/SerializationFilesManager.java b/src/main/java/com/android/tools/build/bundletool/io/SerializationFilesManager.java new file mode 100644 index 00000000..ac877f9e --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/SerializationFilesManager.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import com.android.tools.build.bundletool.model.utils.ZipUtils; +import com.google.common.io.Closeables; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipFile; + +/** Manages temp directory and all files opened during APK serialization. */ +class SerializationFilesManager implements AutoCloseable { + private static final String RESOURCES_ENTRIES_ZIPPED_PACK = "resources-pack.zip"; + private static final String COMPRESSED_ENTRIES_PACK = "compressed.zip"; + private static final String COMPRESSED_RESOURCE_ENTRIES_PACK = "compressed-res.zip"; + private static final String UNCOMPRESSED_ENTRIES_PACK = "uncompressed.zip"; + + private final TempDirectory tempDirectory = new TempDirectory(); + private final AtomicInteger counter = new AtomicInteger(); + private final ConcurrentHashMap openedBinaryApks = new ConcurrentHashMap<>(); + + @Override + public void close() throws IOException { + for (ZipFile zipFile : openedBinaryApks.values()) { + Closeables.close(zipFile, /* swallowIOException= */ true); + } + tempDirectory.close(); + } + + Path getRootDirectory() { + return tempDirectory.getPath(); + } + + Path getResourcesEntriesPackPath() { + return tempDirectory.getPath().resolve(RESOURCES_ENTRIES_ZIPPED_PACK); + } + + Path getCompressedResourceEntriesPackPath() { + return tempDirectory.getPath().resolve(COMPRESSED_RESOURCE_ENTRIES_PACK); + } + + Path getCompressedEntriesPackPath() { + return tempDirectory.getPath().resolve(COMPRESSED_ENTRIES_PACK); + } + + Path getUncompressedEntriesPackPath() { + return tempDirectory.getPath().resolve(UNCOMPRESSED_ENTRIES_PACK); + } + + Path getNextAapt2ProtoApkPath() { + return tempDirectory.getPath().resolve("proto" + counter.incrementAndGet() + ".apk"); + } + + Path getNextAapt2BinaryApkPath() { + return tempDirectory.getPath().resolve("binary" + counter.incrementAndGet() + ".apk"); + } + + ZipFile openBinaryApk(Path zipPath) { + return openedBinaryApks.computeIfAbsent(zipPath, ZipUtils::openZipFile); + } + + void closeAndRemoveBinaryApks() throws IOException { + for (Entry opened : openedBinaryApks.entrySet()) { + Closeables.close(opened.getValue(), /* swallowIOException= */ true); + Files.delete(opened.getKey()); + } + openedBinaryApks.clear(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/SplitApkSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/SplitApkSerializer.java deleted file mode 100644 index ab2bf293..00000000 --- a/src/main/java/com/android/tools/build/bundletool/io/SplitApkSerializer.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 - */ - -package com.android.tools.build.bundletool.io; - -import static com.google.common.base.Preconditions.checkState; -import static java.nio.file.Files.isDirectory; - -import com.android.bundle.Commands.ApkDescription; -import com.android.bundle.Commands.SplitApkMetadata; -import com.android.tools.build.bundletool.model.ModuleSplit; -import com.android.tools.build.bundletool.model.ZipPath; -import java.nio.file.Path; -import java.util.function.BiFunction; -import javax.inject.Inject; - -/** Serializes split APKs on disk. */ -public class SplitApkSerializer { - - private final ApkSerializerHelper apkSerializerHelper; - - @Inject - public SplitApkSerializer(ApkSerializerHelper apkSerializerHelper) { - this.apkSerializerHelper = apkSerializerHelper; - } - - /** Writes the installable split to disk. */ - public ApkDescription writeSplitToDisk(ModuleSplit split, Path outputDirectory, ZipPath apkPath) { - return writeToDisk( - split, outputDirectory, ApkDescription.Builder::setSplitApkMetadata, apkPath); - } - - /** Writes the instant split to disk. */ - public ApkDescription writeInstantSplitToDisk( - ModuleSplit split, Path outputDirectory, ZipPath apkPath) { - return writeToDisk( - split, outputDirectory, ApkDescription.Builder::setInstantApkMetadata, apkPath); - } - - /** Writes the asset slice to disk. */ - public ApkDescription writeAssetSliceToDisk( - ModuleSplit split, Path outputDirectory, ZipPath apkPath) { - return writeToDisk( - split, outputDirectory, ApkDescription.Builder::setAssetSliceMetadata, apkPath); - } - - /** Writes the given split to the path subdirectory in the APK Set. */ - private ApkDescription writeToDisk( - ModuleSplit split, - Path outputDirectory, - BiFunction setApkMetadata, - ZipPath apkPath) { - checkState(isDirectory(outputDirectory), "Output directory does not exist."); - - apkSerializerHelper.writeToZipFile(split, outputDirectory.resolve(apkPath.toString())); - ApkDescription.Builder builder = - ApkDescription.newBuilder() - .setPath(apkPath.toString()) - .setTargeting(split.getApkTargeting()); - return setApkMetadata - .apply( - builder, - SplitApkMetadata.newBuilder() - .setSplitId(split.getAndroidManifest().getSplitId().orElse("")) - .setIsMasterSplit(split.isMasterSplit()) - .build()) - .build(); - } -} diff --git a/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java deleted file mode 100644 index 9bd2da97..00000000 --- a/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 - */ - -package com.android.tools.build.bundletool.io; - -import com.android.bundle.Commands; -import com.android.bundle.Commands.ApexApkMetadata; -import com.android.bundle.Commands.ApkDescription; -import com.android.bundle.Commands.SplitApkMetadata; -import com.android.bundle.Commands.StandaloneApkMetadata; -import com.android.bundle.Commands.SystemApkMetadata; -import com.android.tools.build.bundletool.model.ModuleSplit; -import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; -import com.android.tools.build.bundletool.model.ZipPath; -import com.google.common.annotations.VisibleForTesting; -import java.nio.file.Path; -import javax.inject.Inject; - -/** Serializes standalone APKs to disk. */ -public class StandaloneApkSerializer { - - private final ApkSerializerHelper apkSerializerHelper; - - @Inject - public StandaloneApkSerializer(ApkSerializerHelper apkSerializerHelper) { - this.apkSerializerHelper = apkSerializerHelper; - } - - public ApkDescription writeToDisk( - ModuleSplit standaloneSplit, Path outputDirectory, ZipPath apkPath) { - return writeToDiskInternal(standaloneSplit, outputDirectory, apkPath); - } - - public ApkDescription writeToDiskAsUniversal(ModuleSplit standaloneSplit, Path outputDirectory) { - return writeToDiskInternal(standaloneSplit, outputDirectory, ZipPath.create("universal.apk")); - } - - public ApkDescription writeHibernatedApkToDisk( - ModuleSplit standaloneSplit, Path outputDirectory, ZipPath apkPath) { - return writeToDiskInternal(standaloneSplit, outputDirectory, apkPath); - } - - public ApkDescription writeSystemApkToDisk( - ModuleSplit systemSplit, Path outputDirectory, ZipPath apkPath) { - apkSerializerHelper.writeToZipFile(systemSplit, outputDirectory.resolve(apkPath.toString())); - - ApkDescription.Builder apkDescription = - ApkDescription.newBuilder() - .setPath(apkPath.toString()) - .setTargeting(systemSplit.getApkTargeting()); - - if (systemSplit.isBaseModuleSplit() && systemSplit.isMasterSplit()) { - apkDescription.setSystemApkMetadata( - SystemApkMetadata.newBuilder() - .addAllFusedModuleName(systemSplit.getAndroidManifest().getFusedModuleNames())); - } else { - apkDescription.setSplitApkMetadata( - SplitApkMetadata.newBuilder() - // Only the base master split doesn't have a split id. - .setSplitId(systemSplit.getAndroidManifest().getSplitId().get()) - .setIsMasterSplit(systemSplit.isMasterSplit())); - } - return apkDescription.build(); - } - - @VisibleForTesting - ApkDescription writeToDiskInternal( - ModuleSplit standaloneSplit, Path outputDirectory, ZipPath apkPath) { - apkSerializerHelper.writeToZipFile( - standaloneSplit, outputDirectory.resolve(apkPath.toString())); - - ApkDescription.Builder apkDescription = - ApkDescription.newBuilder() - .setPath(apkPath.toString()) - .setTargeting(standaloneSplit.getApkTargeting()); - - if (standaloneSplit.isApex()) { - apkDescription.setApexApkMetadata( - ApexApkMetadata.newBuilder() - .addAllApexEmbeddedApkConfig(standaloneSplit.getApexEmbeddedApkConfigs()) - .build()); - } else if (standaloneSplit.getSplitType() == SplitType.HIBERNATION) { - apkDescription.setHibernatedApkMetadata(Commands.HibernatedApkMetadata.getDefaultInstance()); - } else { - apkDescription.setStandaloneApkMetadata( - StandaloneApkMetadata.newBuilder() - .addAllFusedModuleName(standaloneSplit.getAndroidManifest().getFusedModuleNames())); - } - - return apkDescription.build(); - } -} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipEntrySource.java b/src/main/java/com/android/tools/build/bundletool/io/ZipEntrySource.java index 6127eb9c..7df4829e 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipEntrySource.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipEntrySource.java @@ -287,7 +287,6 @@ public long size() { /** A {@link Payload} read from a zip and uncompressed on the fly. */ private static final class UncompressedPayload extends Payload { - private static final int BUFFER_SIZE_BYTES = 8192; private final ZipReader zipReader; private final Entry entry; @@ -299,7 +298,7 @@ private static final class UncompressedPayload extends Payload { @Override public long writeTo(ZipWriter writer) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE_BYTES); + ByteBuffer buffer = ByteBuffer.allocate(ZipReader.BUFFER_SIZE_BYTES); int totalBytes = 0; try (InputStream in = zipReader.getUncompressedPayload(entry.getName())) { diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializerHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializer.java similarity index 90% rename from src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializerHelper.java rename to src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializer.java index 307796c7..fb5874c0 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializerHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerApkSerializer.java @@ -15,6 +15,8 @@ */ package com.android.tools.build.bundletool.io; +import static com.android.tools.build.bundletool.io.ApkSerializerHelper.NO_COMPRESSION_EXTENSIONS; +import static com.android.tools.build.bundletool.io.ApkSerializerHelper.requiresAapt2Conversion; import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; import static com.android.tools.build.bundletool.model.CompressionLevel.BEST_COMPRESSION; import static com.android.tools.build.bundletool.model.CompressionLevel.DEFAULT_COMPRESSION; @@ -32,10 +34,13 @@ import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; import static java.util.Comparator.naturalOrder; +import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Config.BundleConfig; import com.android.bundle.Config.ResourceOptimizations.SparseEncoding; import com.android.tools.build.bundletool.androidtools.Aapt2Command; import com.android.tools.build.bundletool.commands.BuildApksManagerComponent.UseBundleCompression; +import com.android.tools.build.bundletool.commands.BuildApksModule.VerboseLogs; +import com.android.tools.build.bundletool.model.ApkListener; import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; import com.android.tools.build.bundletool.model.CompressionLevel; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -51,6 +56,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -62,7 +70,7 @@ import javax.inject.Inject; /** Serializes APKs to Proto or Binary format. */ -final class ZipFlingerApkSerializerHelper extends ApkSerializerHelper { +public final class ZipFlingerApkSerializer extends ApkSerializer { /** Suffix for native libraries. */ private static final String NATIVE_LIBRARIES_SUFFIX = ".so"; @@ -75,6 +83,7 @@ final class ZipFlingerApkSerializerHelper extends ApkSerializerHelper { private final Aapt2Command aapt2; private final Version bundletoolVersion; private final boolean enableSparseEncoding; + private final ListeningExecutorService executorService; /** * Whether to re-use the compression of the entries in the App Bundle. @@ -85,13 +94,17 @@ final class ZipFlingerApkSerializerHelper extends ApkSerializerHelper { private final boolean useBundleCompression; @Inject - ZipFlingerApkSerializerHelper( + ZipFlingerApkSerializer( + Optional apkListener, + @VerboseLogs boolean verbose, ZipReader bundleZipReader, BundleConfig bundleConfig, Aapt2Command aapt2, Version bundletoolVersion, ApkSigner apkSigner, - @UseBundleCompression boolean useBundleCompression) { + @UseBundleCompression boolean useBundleCompression, + ListeningExecutorService executorService) { + super(apkListener, verbose); this.bundleZipReader = bundleZipReader; this.bundleConfig = bundleConfig; this.aapt2 = aapt2; @@ -104,9 +117,27 @@ final class ZipFlingerApkSerializerHelper extends ApkSerializerHelper { .getResourceOptimizations() .getSparseEncoding() .equals(SparseEncoding.ENFORCED); + this.executorService = executorService; } @Override + public ImmutableMap serialize( + Path outputDirectory, ImmutableMap splitsByRelativePath) { + Map> serializedSplits = + Maps.transformEntries( + splitsByRelativePath, + (relativePath, split) -> + executorService.submit( + () -> { + writeToZipFile(split, outputDirectory.resolve(relativePath.toString())); + ApkDescription apkDescription = + ApkDescriptionHelper.createApkDescription(relativePath, split); + notifyApkSerialized(apkDescription, split.getSplitType()); + return apkDescription; + })); + return ConcurrencyUtils.waitForAll(serializedSplits); + } + public Path writeToZipFile(ModuleSplit split, Path outputPath) { try (TempDirectory tempDir = new TempDirectory(getClass().getSimpleName())) { writeToZipFile(split, outputPath, tempDir); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipReader.java b/src/main/java/com/android/tools/build/bundletool/io/ZipReader.java index 468cd4a9..2254147f 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipReader.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipReader.java @@ -43,6 +43,8 @@ /** Parses a zip file, and allows to read entries and their content. */ public final class ZipReader implements AutoCloseable { + static final int BUFFER_SIZE_BYTES = 8192; + /** The parsed map of the zip file. */ private final ZipMap zipMap; @@ -109,7 +111,7 @@ public InputStream getUncompressedPayload(String entryName) { return entryPayload; } Inflater inflater = new Inflater(/* nowrap= */ true); // nowrap = gzip compatible - return new InflaterInputStream(entryPayload, inflater); + return new InflaterInputStream(entryPayload, inflater, BUFFER_SIZE_BYTES); } private InputStream getEntryPayload(Entry entry) { diff --git a/src/main/java/com/android/tools/build/bundletool/io/Zipper.java b/src/main/java/com/android/tools/build/bundletool/io/Zipper.java new file mode 100644 index 00000000..101d9867 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/Zipper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.io; + +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.zipflinger.Source; +import com.android.zipflinger.Sources; +import com.android.zipflinger.ZipArchive; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteSource; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.zip.Deflater; + +/** Interface to zip pairs of name/content to zip archive. */ +interface Zipper { + /** + * Threshold for an entry size below which we consider that compressing its payload in a separate + * thread would not provide any speed benefit. + * + *

Magic number found empirically. Can be overridden using the system property + * "bundletool.compression.newthread.entrysize". + */ + long LARGE_ENTRY_SIZE_THRESHOLD_BYTES = + SystemEnvironmentProvider.DEFAULT_PROVIDER + .getProperty("bundletool.compression.newthread.entrysize") + .map(Long::parseLong) + .orElse(100_000L); + + /** Zips pairs of name/content into zip archive. */ + void zip(Path outputZip, ImmutableMap entries); + + /** Creates instance of {@link Zipper} which creates ZIP with uncompressed entries. */ + static Zipper uncompressedZip() { + return (outputZip, entries) -> { + try (ZipArchive archive = new ZipArchive(outputZip)) { + for (Map.Entry entry : entries.entrySet()) { + archive.add( + Sources.from(entry.getValue().openStream(), entry.getKey(), Deflater.NO_COMPRESSION)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } + + /** Creates instance of {@link Zipper} which creates ZIP with compressed entries. */ + static Zipper compressedZip(ListeningExecutorService executorService, int compressionLevel) { + return (outputZip, entries) -> { + try (ZipArchive archive = new ZipArchive(outputZip)) { + ImmutableList.Builder> largeSources = ImmutableList.builder(); + for (Map.Entry entry : entries.entrySet()) { + String path = entry.getKey(); + ByteSource content = entry.getValue(); + boolean smallEntry = + content + .sizeIfKnown() + .transform(size -> size < LARGE_ENTRY_SIZE_THRESHOLD_BYTES) + .or(false); + + if (smallEntry) { + archive.add(Sources.from(content.openStream(), path, compressionLevel)); + } else { + largeSources.add( + executorService.submit( + () -> Sources.from(content.openStream(), path, compressionLevel))); + } + } + for (Future source : Futures.inCompletionOrder(largeSources.build())) { + archive.add(Futures.getUnchecked(source)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java b/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java index 4f922d29..1be3e14f 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java @@ -44,6 +44,7 @@ import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.TextureCompressionFormatTargeting; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.google.common.collect.Sets; import com.google.protobuf.Int32Value; import java.util.List; @@ -77,6 +78,7 @@ public static Optional getSameValueOrNonNull(@Nullable T nullableValue, T * Merges two targetings into targeting of an APK shard. * *

Supports only the following targetings: + * *

    *
  • ABI *
  • Screen density @@ -129,9 +131,11 @@ public static void mergeTargetedAssetsDirectories( String path = directory.getPath(); if (assetsDirectories.containsKey(path)) { TargetedAssetsDirectory existingDirectory = assetsDirectories.get(path); - if (!existingDirectory.equals(directory)) { - throw new IllegalStateException( - "Encountered conflicting targeting values while merging assets config."); + if (!existingDirectory.getTargeting().equals(directory.getTargeting())) { + throw InvalidBundleException.builder() + .withUserMessage( + "Encountered conflicting targeting values while merging assets config.") + .build(); } } else { assetsDirectories.put(path, directory); diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java index a921163d..3a425e2e 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java @@ -65,6 +65,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -316,7 +317,12 @@ static ImmutableList renameDexFromAllModulesToSingleShard( dexFilesToMergeByModule.keys().stream() .distinct() .filter(moduleName -> !BASE_MODULE_NAME.equals(moduleName)) - .flatMap(moduleName -> dexFilesToMergeByModule.get(moduleName).stream()); + // Sort module .dex files by their index to ensure in the final output they are in the + // same order as were defined in module. + .flatMap( + moduleName -> + dexFilesToMergeByModule.get(moduleName).stream() + .sorted(Comparator.comparingInt(ModuleSplitsToShardMerger::getDexIndex))); Stream renamedDexFiles = Streams.mapWithIndex( @@ -333,6 +339,19 @@ static ImmutableList renameDexFromAllModulesToSingleShard( return Stream.concat(dexFilesFromBase, renamedDexFiles).collect(toImmutableList()); } + private static int getDexIndex(ModuleEntry entry) { + String fileName = entry.getPath().getFileName().toString(); + if (!(fileName.startsWith("classes") && fileName.endsWith(".dex"))) { + return -1; + } + // Magic numbers: 7 - length of "classes" and 4 - length of ".dex" + String index = fileName.substring(7, fileName.length() - 4); + if (!index.chars().allMatch(Character::isDigit)) { + return -1; + } + return index.isEmpty() ? 1 : Integer.parseInt(index); + } + private ImmutableList mergeDexFiles( List dexEntries, AndroidManifest androidManifest) { try { diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/ResourceTableMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/ResourceTableMerger.java index 8daf08ef..16d06980 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/ResourceTableMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/ResourceTableMerger.java @@ -36,6 +36,7 @@ import com.google.common.collect.Sets; import com.google.common.collect.Streams; import com.google.common.primitives.Ints; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Message; @@ -45,7 +46,6 @@ import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; -import javax.annotation.CheckReturnValue; /** * Recursively merges two resource tables. diff --git a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java index 8f40a23a..6e84b4fe 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -43,12 +43,13 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import com.google.common.primitives.Ints; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import java.util.Optional; import java.util.stream.Stream; -import javax.annotation.CheckReturnValue; /** * Represents Android manifest. @@ -78,6 +79,9 @@ public abstract class AndroidManifest { public static final String PROVIDER_ELEMENT_NAME = "provider"; public static final String SUPPORTS_GL_TEXTURE_ELEMENT_NAME = "supports-gl-texture"; public static final String ACTION_ELEMENT_NAME = "action"; + public static final String PERMISSION_ELEMENT_NAME = "permission"; + public static final String PERMISSION_GROUP_ELEMENT_NAME = "permission-group"; + public static final String PERMISSION_TREE_ELEMENT_NAME = "permission-tree"; public static final String DEBUGGABLE_ATTRIBUTE_NAME = "debuggable"; public static final String EXTRACT_NATIVE_LIBS_ATTRIBUTE_NAME = "extractNativeLibs"; @@ -113,6 +117,12 @@ public abstract class AndroidManifest { public static final String BACKUP_AGENT_ATTRIBUTE_NAME = "backupAgent"; public static final String DATA_EXTRACTION_RULES_ATTRIBUTE_NAME = "dataExtractionRules"; public static final String EXPORTED_ATTRIBUTE_NAME = "exported"; + public static final String LOCALE_CONFIG_ATTRIBUTE_NAME = "localeConfig"; + public static final String SDK_LIBRARY_ELEMENT_NAME = "sdk-library"; + public static final String SDK_MAJOR_VERSION_ATTRIBUTE_NAME = "versionMajor"; + public static final String SDK_PATCH_VERSION_ATTRIBUTE_NAME = + "com.android.vending.sdkPatchVersion"; + public static final Integer SDK_SANDBOX_MIN_VERSION = 32; public static final String MODULE_TYPE_FEATURE_VALUE = "feature"; public static final String MODULE_TYPE_ASSET_VALUE = "asset-pack"; @@ -153,6 +163,7 @@ public abstract class AndroidManifest { public static final int BACKUP_AGENT_RESOURCE_ID = 0x0101027f; public static final int DATA_EXTRACTION_RULES_RESOURCE_ID = 0x0101063e; public static final int EXPORTED_RESOURCE_ID = 0x01010010; + public static final int LOCALE_CONFIG_RESOURCE_ID = 0x01df0009; // Matches the value of android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, used when turning // a manifest attribute which references a prerelease API version (e.g., "Q") into an integer. @@ -602,6 +613,13 @@ public boolean hasLabelRefId() { return hasApplicationAttributeAsRefId(LABEL_RESOURCE_ID); } + public boolean hasLocaleConfig() { + return getManifestElement() + .getOptionalChildElement(APPLICATION_ELEMENT_NAME) + .flatMap(app -> app.getAndroidAttribute(LOCALE_CONFIG_RESOURCE_ID)) + .isPresent(); + } + public Optional getLabelString() { return getApplicationAttribute(LABEL_RESOURCE_ID); } @@ -765,6 +783,27 @@ public ModuleDeliveryType getInstantModuleDeliveryType() { return NO_INITIAL_INSTALL; } + /** Returns a list of the XML elements. */ + public ImmutableList getPermissions() { + return getManifestElement() + .getChildrenElements(PERMISSION_ELEMENT_NAME) + .collect(toImmutableList()); + } + + /** Returns a list of the XML elements. */ + public ImmutableList getPermissionGroups() { + return getManifestElement() + .getChildrenElements(PERMISSION_GROUP_ELEMENT_NAME) + .collect(toImmutableList()); + } + + /** Returns a list of the XML elements. */ + public ImmutableList getPermissionTrees() { + return getManifestElement() + .getChildrenElements(PERMISSION_TREE_ELEMENT_NAME) + .collect(toImmutableList()); + } + /** Returns a stream of the XML elements under the tag. */ private Stream getMetadataElements() { return getManifestElement() @@ -820,6 +859,15 @@ private boolean hasApplicationAttributeAsString(int resourceId) { .orElse(false); } + public ImmutableList getSdkLibraryElements() { + return getManifestRoot() + .getElement() + .getChildElement(APPLICATION_ELEMENT_NAME) + .getChildrenElements() + .filter(elem -> elem.getName().equals(SDK_LIBRARY_ELEMENT_NAME)) + .collect(toImmutableList()); + } + private boolean hasApplicationAttributeAsRefId(int resourceId) { return getManifestElement() .getOptionalChildElement(APPLICATION_ELEMENT_NAME) @@ -827,4 +875,17 @@ private boolean hasApplicationAttributeAsRefId(int resourceId) { .map(XmlProtoAttribute::hasRefIdValue) .orElse(false); } + + /** Checks whether any component elements are declared. */ + public boolean hasComponents() { + ImmutableSet componentNames = + ImmutableSet.of( + ACTIVITY_ELEMENT_NAME, + SERVICE_ELEMENT_NAME, + PROVIDER_ELEMENT_NAME, + RECEIVER_ELEMENT_NAME); + return stream(getManifestElement().getOptionalChildElement(APPLICATION_ELEMENT_NAME)) + .flatMap(app -> app.getChildrenElements()) + .anyMatch(component -> componentNames.contains(component.getName())); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ApkModifier.java b/src/main/java/com/android/tools/build/bundletool/model/ApkModifier.java index ce4a7a3d..4b02b556 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ApkModifier.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ApkModifier.java @@ -18,8 +18,8 @@ import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; -import javax.annotation.CheckReturnValue; /** Modifier of APKs. */ public abstract class ApkModifier { diff --git a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java index f909d571..48c221fd 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -16,8 +16,11 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.utils.BundleParser.extractModules; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readBundleConfig; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readBundleMetadata; +import static com.android.tools.build.bundletool.model.utils.BundleParser.sanitize; import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.MoreCollectors.onlyElement; @@ -30,9 +33,6 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.NativeDirectoryTargeting; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; -import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; -import com.android.tools.build.bundletool.model.utils.ZipUtils; import com.android.tools.build.bundletool.model.version.Version; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; @@ -40,19 +40,9 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.InvalidProtocolBufferException; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Paths; import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; import java.util.stream.Stream; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import javax.annotation.CheckReturnValue; /** * Represents an app bundle. @@ -62,17 +52,21 @@ */ @Immutable @AutoValue -public abstract class AppBundle { +public abstract class AppBundle implements Bundle { public static final ZipPath METADATA_DIRECTORY = ZipPath.create("BUNDLE-METADATA"); public static final String BUNDLE_CONFIG_FILE_NAME = "BundleConfig.pb"; + /** Top-level directory names that are not recognized as modules. */ + public static final ImmutableSet NON_MODULE_DIRECTORIES = + ImmutableSet.of(METADATA_DIRECTORY, ZipPath.create("META-INF")); + /** Builds an {@link AppBundle} from an App Bundle on disk. */ public static AppBundle buildFromZip(ZipFile bundleFile) { BundleConfig bundleConfig = readBundleConfig(bundleFile); return buildFromModules( - sanitize(extractModules(bundleFile, bundleConfig)), + sanitize(extractModules(bundleFile, bundleConfig, NON_MODULE_DIRECTORIES)), bundleConfig, readBundleMetadata(bundleFile)); } @@ -111,8 +105,10 @@ public static AppBundle buildFromModules( */ public abstract ImmutableSet getMasterPinnedResourceNames(); + @Override public abstract BundleConfig getBundleConfig(); + @Override public abstract BundleMetadata getBundleMetadata(); /** @@ -137,6 +133,7 @@ public BundleModule getBaseModule() { return getModule(BundleModuleName.BASE_MODULE_NAME); } + @Override public String getPackageName() { if (isAssetOnly()) { return getModules().values().stream() @@ -147,6 +144,7 @@ public String getPackageName() { return getBaseModule().getAndroidManifest().getPackageName(); } + @Override public BundleModule getModule(BundleModuleName moduleName) { BundleModule module = getModules().get(moduleName); checkState(module != null, "Module '%s' not found.", moduleName); @@ -182,36 +180,6 @@ public ImmutableSet getTargetedAbis() { .collect(toImmutableSet()); } - /** - * Returns the {@link BundleModuleName} corresponding to the provided zip entry. If the zip entry - * does not belong to a module, a null {@link BundleModuleName} is returned. - */ - public static Optional extractModuleName(ZipEntry entry) { - ZipPath path = ZipPath.create(entry.getName()); - - // Ignoring bundle metadata files. - if (path.startsWith(METADATA_DIRECTORY)) { - return Optional.empty(); - } - - // Ignoring signature related files. - if (path.startsWith("META-INF")) { - return Optional.empty(); - } - - // Ignoring top-level files. - if (path.getNameCount() <= 1) { - return Optional.empty(); - } - - // Temporarily excluding .class files. - if (path.toString().endsWith(".class")) { - return Optional.empty(); - } - - return Optional.of(BundleModuleName.create(path.getName(0).toString())); - } - public boolean isApex() { return !isAssetOnly() && getBaseModule().getApexConfig().isPresent(); } @@ -252,104 +220,6 @@ static Builder builder() { return new AutoValue_AppBundle.Builder(); } - private static ImmutableList extractModules( - ZipFile bundleFile, BundleConfig bundleConfig) { - Map moduleBuilders = new HashMap<>(); - Enumeration entries = bundleFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - continue; - } - - Optional moduleName = extractModuleName(entry); - if (!moduleName.isPresent()) { - continue; - } - - BundleModule.Builder moduleBuilder = - moduleBuilders.computeIfAbsent( - moduleName.get(), - name -> BundleModule.builder().setName(name).setBundleConfig(bundleConfig)); - - moduleBuilder.addEntry( - ModuleEntry.builder() - .setBundleLocation( - ModuleEntryBundleLocation.create( - Paths.get(bundleFile.getName()), ZipPath.create(entry.getName()))) - .setPath(ZipUtils.convertBundleToModulePath(ZipPath.create(entry.getName()))) - .setContent(ZipUtils.asByteSource(bundleFile, entry)) - .build()); - } - - // We verify the presence of the manifest before building the BundleModule objects because the - // manifest is a required field of the BundleModule class. - checkModulesHaveManifest(moduleBuilders.values()); - - return moduleBuilders.values().stream() - .map(BundleModule.Builder::build) - .collect(toImmutableList()); - } - - private static void checkModulesHaveManifest(Collection bundleModules) { - ImmutableSet modulesWithoutManifest = - bundleModules.stream() - .filter(bundleModule -> !bundleModule.hasAndroidManifest()) - .map(module -> module.getName().getName()) - .collect(toImmutableSet()); - if (!modulesWithoutManifest.isEmpty()) { - throw InvalidBundleException.builder() - .withUserMessage( - "Found modules in the App Bundle without an AndroidManifest.xml: %s", - modulesWithoutManifest) - .build(); - } - } - - private static BundleConfig readBundleConfig(ZipFile bundleFile) { - ZipEntry bundleConfigEntry = bundleFile.getEntry(BUNDLE_CONFIG_FILE_NAME); - if (bundleConfigEntry == null) { - throw InvalidBundleException.builder() - .withUserMessage("File '%s' was not found.", BUNDLE_CONFIG_FILE_NAME) - .build(); - } - - try { - return BundleConfig.parseFrom(ZipUtils.asByteSource(bundleFile, bundleConfigEntry).read()); - } catch (InvalidProtocolBufferException e) { - throw InvalidBundleException.builder() - .withCause(e) - .withUserMessage("Bundle config '%s' could not be parsed.", BUNDLE_CONFIG_FILE_NAME) - .build(); - } catch (IOException e) { - throw new UncheckedIOException( - String.format("Error reading file '%s'.", BUNDLE_CONFIG_FILE_NAME), e); - } - } - - private static BundleMetadata readBundleMetadata(ZipFile bundleFile) { - BundleMetadata.Builder metadata = BundleMetadata.builder(); - ZipUtils.allFileEntries(bundleFile) - .filter(entry -> ZipPath.create(entry.getName()).startsWith(METADATA_DIRECTORY)) - .forEach( - zipEntry -> { - ZipPath bundlePath = ZipPath.create(zipEntry.getName()); - // Strip the top-level metadata directory. - ZipPath metadataPath = bundlePath.subpath(1, bundlePath.getNameCount()); - metadata.addFile(metadataPath, ZipUtils.asByteSource(bundleFile, zipEntry)); - }); - return metadata.build(); - } - - @CheckReturnValue - private static ImmutableList sanitize(ImmutableList modules) { - // This is a temporary fix to work around a bug in gradle that creates a file named classes1.dex - modules = - modules.stream().map(new ClassesDexNameSanitizer()::sanitize).collect(toImmutableList()); - - return modules; - } - /** Builder for App Bundle object */ @AutoValue.Builder public abstract static class Builder { diff --git a/src/main/java/com/android/tools/build/bundletool/model/Bundle.java b/src/main/java/com/android/tools/build/bundletool/model/Bundle.java new file mode 100644 index 00000000..0392505f --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/Bundle.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model; + +import com.android.bundle.Config.BundleConfig; + +/** Interface for bundles. */ +public interface Bundle { + String getPackageName(); + + BundleConfig getBundleConfig(); + + BundleModule getModule(BundleModuleName moduleName); + + BundleMetadata getBundleMetadata(); +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java index 731ff13a..42d6e3af 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java +++ b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java @@ -31,6 +31,7 @@ import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.ModuleTargeting; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.version.BundleToolVersion; @@ -110,7 +111,7 @@ public boolean isFeatureModule() { } } - /** The version of Bundletool that built this module, taken from BundleConfig. */ + /** BundleConfig of the bundle that this module belongs to. */ public abstract BundleConfig getBundleConfig(); abstract XmlNode getAndroidManifestProto(); @@ -129,6 +130,8 @@ public AndroidManifest getAndroidManifest() { public abstract Optional getApexConfig(); + public abstract Optional getRuntimeEnabledSdkConfig(); + /** * Returns entries of the module, indexed by their module path. * @@ -311,6 +314,9 @@ public Builder setAndroidManifest(AndroidManifest androidManifest) { public abstract Builder setApexConfig(ApexImages apexConfig); + public abstract Builder setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig); + abstract ImmutableMap.Builder entryMapBuilder(); abstract Builder setEntryMap(ImmutableMap entryMap); @@ -409,6 +415,12 @@ void addToModule(BundleModule.Builder module, InputStream inputStream) throws IO void addToModule(BundleModule.Builder module, InputStream inputStream) throws IOException { module.setApexConfig(ApexImages.parseFrom(inputStream)); } + }, + RUNTIME_ENABLED_SDK_CONFIG("runtime_enabled_sdk_config.pb") { + @Override + void addToModule(BundleModule.Builder module, InputStream inputStream) throws IOException { + module.setRuntimeEnabledSdkConfig(RuntimeEnabledSdkConfig.parseFrom(inputStream)); + } }; private static final ImmutableMap SPECIAL_ENTRY_BY_PATH = diff --git a/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java b/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java index d898fe9f..1c67d122 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java @@ -22,11 +22,11 @@ import static java.util.stream.Collectors.partitioningBy; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.Map; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.CheckReturnValue; /** * Sanitizer of the name of dex files to workaround a Gradle plugin bug that creates a bundle with a diff --git a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java index 6245b93c..bea5c165 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java +++ b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java @@ -43,15 +43,15 @@ public abstract class GeneratedApks { public abstract ImmutableList getSystemApks(); - // There is alsways a single hibernated APK. List type is used for consistency. - public abstract ImmutableList getHibernatedApks(); + // There is alsways a single archived APK. List type is used for consistency. + public abstract ImmutableList getArchivedApks(); public int size() { return getInstantApks().size() + getSplitApks().size() + getStandaloneApks().size() + getSystemApks().size() - + getHibernatedApks().size(); + + getArchivedApks().size(); } public Stream getAllApksStream() { @@ -60,7 +60,7 @@ public Stream getAllApksStream() { getInstantApks(), getSplitApks(), getSystemApks(), - getHibernatedApks()) + getArchivedApks()) .flatMap(List::stream); } @@ -76,7 +76,7 @@ public static Builder builder() { .setSplitApks(ImmutableList.of()) .setStandaloneApks(ImmutableList.of()) .setSystemApks(ImmutableList.of()) - .setHibernatedApks(ImmutableList.of()); + .setArchivedApks(ImmutableList.of()); } /** Creates a GeneratedApk instance from a list of module splits. */ @@ -88,7 +88,7 @@ public static GeneratedApks fromModuleSplits(ImmutableList moduleSp .setSplitApks(groups.getOrDefault(SplitType.SPLIT, ImmutableList.of())) .setStandaloneApks(groups.getOrDefault(SplitType.STANDALONE, ImmutableList.of())) .setSystemApks(groups.getOrDefault(SplitType.SYSTEM, ImmutableList.of())) - .setHibernatedApks(groups.getOrDefault(SplitType.HIBERNATION, ImmutableList.of())) + .setArchivedApks(groups.getOrDefault(SplitType.ARCHIVE, ImmutableList.of())) .build(); } @@ -104,7 +104,7 @@ public abstract static class Builder { public abstract Builder setSystemApks(ImmutableList systemApks); - public abstract Builder setHibernatedApks(ImmutableList hibernatedApks); + public abstract Builder setArchivedApks(ImmutableList archivedApks); public abstract GeneratedApks build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index dbf16792..f0d43530 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -43,6 +43,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MAX_SDK_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.MAX_SDK_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_ELEMENT_NAME; @@ -81,8 +83,8 @@ import com.android.tools.build.bundletool.model.version.Version; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.Optional; -import javax.annotation.CheckReturnValue; /** Modifies the manifest in the protocol buffer format. */ public class ManifestEditor { @@ -194,6 +196,11 @@ public ManifestEditor setSharedUserLabel(Integer valueRefId) { return this; } + public ManifestEditor setLocaleConfig(int resourceId) { + return setApplcationAttributeRefId( + LOCALE_CONFIG_ATTRIBUTE_NAME, LOCALE_CONFIG_RESOURCE_ID, resourceId); + } + public ManifestEditor addMetaDataString(String key, String value) { return addMetaDataValue( key, createAndroidAttribute("value", VALUE_RESOURCE_ID).setValueAsString(value)); @@ -402,6 +409,20 @@ public ManifestEditor addReceiver(Receiver receiver) { return this; } + public ManifestEditor copyPermissions(AndroidManifest manifest) { + manifest + .getPermissions() + .forEach(permission -> manifestElement.addChildElement(permission.toBuilder())); + return this; + } + + public ManifestEditor copyPermissionGroups(AndroidManifest manifest) { + manifest + .getPermissionGroups() + .forEach(permissionGroup -> manifestElement.addChildElement(permissionGroup.toBuilder())); + return this; + } + /** Generates the modified manifest. */ @CheckReturnValue public AndroidManifest save() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleAbiSanitizer.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleAbiSanitizer.java index 0d603ae2..165bb65e 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleAbiSanitizer.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleAbiSanitizer.java @@ -28,10 +28,10 @@ import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.Collection; import java.util.function.Function; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; /** * Makes sure that each "lib/" directory contains the same number of files. If there is a diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java index 8a4c9401..dea93489 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java @@ -17,6 +17,9 @@ import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.Files; import com.google.common.io.MoreFiles; @@ -87,13 +90,16 @@ public final boolean equals(Object obj2) { return false; } + return entry1.getContentSha256Hash().equals(entry2.getContentSha256Hash()); + } + + @Memoized + public HashCode getContentSha256Hash() { try { - return entry1.getContent().contentEquals(entry2.getContent()); + return getContent().hash(Hashing.sha256()); } catch (IOException e) { throw new UncheckedIOException( - String.format( - "Failed to compare contents of module entries '%s' and '%s'.", entry1, entry2), - e); + String.format("Failed to calculate SHA256 hash of module entry '%s'.", this), e); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java index fe6daea2..e5961610 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java @@ -16,6 +16,8 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_SANDBOX_MIN_VERSION; import static com.android.tools.build.bundletool.model.BundleModule.APEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; @@ -61,6 +63,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -70,7 +73,6 @@ import java.util.Optional; import java.util.StringJoiner; import java.util.stream.Stream; -import javax.annotation.CheckReturnValue; /** A module split is a subset of a bundle module. */ @Immutable @@ -80,6 +82,8 @@ public abstract class ModuleSplit { private static final Joiner MULTI_ABI_SUFFIX_JOINER = Joiner.on('.'); + public static final String DEFAULT_SDK_PATCH_VERSION = "0"; + /** The split type being represented by this split. */ public enum SplitType { STANDALONE, @@ -87,7 +91,7 @@ public enum SplitType { SPLIT, INSTANT, ASSET_SLICE, - HIBERNATION, + ARCHIVE, } /** @@ -315,6 +319,47 @@ public ModuleSplit setHasCodeInManifest(boolean hasCode) { return toBuilder().setAndroidManifest(modifiedManifest).build(); } + /** Writes SDK version code to Android Manifest. */ + public ModuleSplit writeSdkVersionCode(Integer versionCode) { + AndroidManifest apkManifest = + getAndroidManifest().toEditor().setVersionCode(versionCode).save(); + return toBuilder().setAndroidManifest(apkManifest).build(); + } + + /** Writes SDK version name ("majorVersion.0.patchVersion") to Android Manifest. */ + public ModuleSplit writeSdkVersionName(String versionName) { + AndroidManifest apkManifest = + getAndroidManifest().toEditor().setVersionName(versionName).save(); + return toBuilder().setAndroidManifest(apkManifest).build(); + } + + /** + * Overrides minimum SDK version if it is lower than the SDK sandbox minimum version or if it is + * not set. + */ + public ModuleSplit overrideMinSdkVersionForSdkSandbox() { + if (!getAndroidManifest().getMinSdkVersion().isPresent() + || getAndroidManifest().getMinSdkVersion().get() < SDK_SANDBOX_MIN_VERSION) { + AndroidManifest apkManifest = + getAndroidManifest().toEditor().setMinSdkVersion(SDK_SANDBOX_MIN_VERSION).save(); + return toBuilder().setAndroidManifest(apkManifest).build(); + } + return this; + } + + /** Sets the SDK Patch version to 0 if it is not already set. */ + public ModuleSplit addDefaultPatchVersionIfNotSet() { + if (!getAndroidManifest().getMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME).isPresent()) { + AndroidManifest apkManifest = + getAndroidManifest() + .toEditor() + .addMetaDataString(SDK_PATCH_VERSION_ATTRIBUTE_NAME, DEFAULT_SDK_PATCH_VERSION) + .save(); + return toBuilder().setAndroidManifest(apkManifest).build(); + } + return this; + } + private String generateSplitId(String resolvedSuffix) { String masterSplitId = getSplitIdForMasterSplit(); if (isMasterSplit()) { @@ -473,30 +518,30 @@ public static ModuleSplit forApex(BundleModule bundleModule, VariantTargeting va variantTargeting); } - public static ModuleSplit forHibernation( + public static ModuleSplit forArchive( BundleModule bundleModule, - AndroidManifest hibernatedManifest, - Optional hibernatedResourceTable, - Path hibernatedClassesDexFile) { - ModuleSplit.Builder hibernatedSplit = + AndroidManifest archivedManifest, + Optional archivedResourceTable, + Path archivedClassesDexFile) { + ModuleSplit.Builder archivedSplit = ModuleSplit.builder() .setModuleName(bundleModule.getName()) - .setSplitType(SplitType.HIBERNATION) + .setSplitType(SplitType.ARCHIVE) .setMasterSplit(true) - .setAndroidManifest(hibernatedManifest) + .setAndroidManifest(archivedManifest) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setVariantTargeting(VariantTargeting.getDefaultInstance()); - if (hibernatedResourceTable.isPresent()) { - hibernatedSplit.setResourceTable(hibernatedResourceTable.get()); - hibernatedSplit.setEntries( - filterResourceEntries(bundleModule.getEntries().asList(), hibernatedResourceTable.get())); + if (archivedResourceTable.isPresent()) { + archivedSplit.setResourceTable(archivedResourceTable.get()); + archivedSplit.setEntries( + filterResourceEntries(bundleModule.getEntries().asList(), archivedResourceTable.get())); } - hibernatedSplit.addEntry( + archivedSplit.addEntry( ModuleEntry.builder() .setPath(DEX_DIRECTORY.resolve("classes.dex")) - .setContent(hibernatedClassesDexFile) + .setContent(archivedClassesDexFile) .build()); - return hibernatedSplit.build(); + return archivedSplit.build(); } /** diff --git a/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java b/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java new file mode 100644 index 00000000..61648bb3 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_MAJOR_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.ModuleSplit.DEFAULT_SDK_PATCH_VERSION; +import static com.android.tools.build.bundletool.model.utils.BundleParser.extractModules; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readBundleConfig; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readBundleMetadata; +import static com.android.tools.build.bundletool.model.utils.BundleParser.sanitize; +import static com.google.common.base.Preconditions.checkState; + +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; +import com.android.tools.build.bundletool.model.version.Version; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.Immutable; +import java.util.zip.ZipFile; + +/** Represents an SDK bundle. */ +@Immutable +@AutoValue +public abstract class SdkBundle implements Bundle { + + /** Top-level directory names that are not recognized as modules. */ + public static final ImmutableSet NON_MODULE_DIRECTORIES = + ImmutableSet.of( + ZipPath.create("BUNDLE-METADATA"), ZipPath.create("META-INF"), ZipPath.create("aar")); + + /** Builds an {@link SdkBundle} from an SDK Bundle on disk. */ + public static SdkBundle buildFromZip(ZipFile bundleFile, Integer versionCode) { + BundleConfig bundleConfig = readBundleConfig(bundleFile); + + return builder() + .setModule( + sanitize(extractModules(bundleFile, bundleConfig, NON_MODULE_DIRECTORIES)).get(0)) + .setBundleConfig(bundleConfig) + .setBundleMetadata(readBundleMetadata(bundleFile)) + .setVersionCode(versionCode) + .build(); + } + + public abstract BundleModule getModule(); + + @Override + public BundleModule getModule(BundleModuleName moduleName) { + checkState(getModule().getName().equals(moduleName), "Module '%s' not found.", moduleName); + return getModule(); + } + + @Override + public abstract BundleConfig getBundleConfig(); + + @Override + public abstract BundleMetadata getBundleMetadata(); + + public abstract Integer getVersionCode(); + + public Version getBundletoolVersion() { + return Version.of(getBundleConfig().getBundletool().getVersion()); + } + + @Override + public String getPackageName() { + return getModule().getAndroidManifest().getPackageName(); + } + + /** + * Gets the Major Version of the SDK bundle. The Major Version is returned as String, but will + * always be parseable as a long. + */ + public String getMajorVersion() { + XmlProtoElement sdkLibraryTag = getSdkLibraryTag(); + return sdkLibraryTag + .getAttribute(ANDROID_NAMESPACE_URI, SDK_MAJOR_VERSION_ATTRIBUTE_NAME) + .get() + .getValueAsString(); + } + + /** + * Gets the Patch Version of the SDK bundle. If Patch Version is not set, {@value + * #DEFAULT_SDK_PATCH_VERSION} is returned. + */ + public String getPatchVersion() { + return getModule() + .getAndroidManifest() + .getMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME) + .orElse(DEFAULT_SDK_PATCH_VERSION); + } + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_SdkBundle.Builder(); + } + + /** Builder for SDK Bundle object */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setModule(BundleModule module); + + public abstract Builder setBundleConfig(BundleConfig bundleConfig); + + public abstract Builder setBundleMetadata(BundleMetadata bundleMetadata); + + public abstract Builder setVersionCode(Integer versionCode); + + public abstract SdkBundle build(); + } + + private XmlProtoElement getSdkLibraryTag() { + return Iterables.getOnlyElement(getModule().getAndroidManifest().getSdkLibraryElements()); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java index f0a45f42..94e28f32 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java +++ b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java @@ -16,7 +16,7 @@ package com.android.tools.build.bundletool.model; -import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.HIBERNATION; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.ARCHIVE; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.INSTANT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SPLIT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.STANDALONE; @@ -54,7 +54,7 @@ public int compareTo(VariantKey o) { // System APKs never occur with other apk types, its ordering position doesn't matter. return comparing( VariantKey::getSplitType, - Ordering.explicit(INSTANT, STANDALONE, SPLIT, HIBERNATION, SYSTEM)) + Ordering.explicit(INSTANT, STANDALONE, SPLIT, ARCHIVE, SYSTEM)) .thenComparing(VariantKey::getVariantTargeting, VARIANT_TARGETING_COMPARATOR) .compare(this, o); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java b/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java index 4308cf87..c51b405b 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java @@ -26,9 +26,9 @@ import com.google.common.collect.Comparators; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import java.util.Comparator; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** diff --git a/src/main/java/com/android/tools/build/bundletool/model/exceptions/InternalExceptionBuilder.java b/src/main/java/com/android/tools/build/bundletool/model/exceptions/InternalExceptionBuilder.java index 66b73295..34051a58 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/exceptions/InternalExceptionBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/model/exceptions/InternalExceptionBuilder.java @@ -18,9 +18,9 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; public class InternalExceptionBuilder { diff --git a/src/main/java/com/android/tools/build/bundletool/model/exceptions/UserExceptionBuilder.java b/src/main/java/com/android/tools/build/bundletool/model/exceptions/UserExceptionBuilder.java index c6c78a3e..6b7a322b 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/exceptions/UserExceptionBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/model/exceptions/UserExceptionBuilder.java @@ -19,9 +19,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.nullToEmpty; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; public class UserExceptionBuilder { diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulator.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulator.java index 68aa20e9..bf8677bf 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulator.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulator.java @@ -35,11 +35,11 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.protobuf.Message; import java.util.Arrays; import java.util.Collection; import java.util.Optional; -import javax.annotation.CheckReturnValue; /** Adds alternative targeting in the {@code T} dimension. */ public abstract class AlternativeVariantTargetingPopulator { @@ -68,7 +68,7 @@ public static GeneratedApks populateAlternativeVariantTargeting( .addAlternativeVariantTargeting(generatedApks.getSplitApks(), standaloneApks)) .addAll(generatedApks.getInstantApks()) .addAll(generatedApks.getSystemApks()) - .addAll(generatedApks.getHibernatedApks()) + .addAll(generatedApks.getArchivedApks()) .build(); return GeneratedApks.fromModuleSplits(moduleSplits); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java index 4d282aa2..0e69dc82 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java @@ -36,10 +36,10 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.common.collect.Range; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.Comparator; import java.util.Optional; import java.util.Set; -import javax.annotation.CheckReturnValue; /** * Selector for the best matching density for a given desired density. diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java index 7bb2d526..6d0614c6 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java @@ -30,6 +30,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.IntStream; /** * Directory with an optional associated targeting. @@ -73,6 +74,21 @@ public String getSubPathBaseName(int maxIndex) { .toString(); } + /** + * Returns the base name of the subpath composed from the first element up to and including the + * element that is targeted by the requested targeting dimension. + * + *

    Example: given directory a/b/c#tier_1/d/e, a request for DEVICE_TIER returns a/b/c. + * + *

    See {@link ZipPath#subpath} and {@link TargetedDirectory#getPathBaseName}. + */ + public String getSubPathBaseName(TargetingDimension dimension) { + Optional targetedSegmentIndex = getTargetedPathSegmentIndex(dimension); + return targetedSegmentIndex.isPresent() + ? getSubPathBaseName(targetedSegmentIndex.get()) + : getPathBaseName(); + } + /** * Returns the value of the targeting for the given dimension, if this dimension is targeted by * this directory. @@ -133,6 +149,16 @@ public ZipPath toZipPath() { return ZipPath.create(pathSegments); } + private Optional getTargetedPathSegmentIndex(TargetingDimension dimension) { + // We're assuming that dimensions are not duplicated (see checkNoDuplicateDimensions). + return IntStream.range(0, getPathSegments().size()) + .filter( + idx -> + getPathSegments().get(idx).getTargetingDimension().equals(Optional.of(dimension))) + .boxed() + .collect(toOptional()); + } + private static void checkNoDuplicateDimensions( ImmutableList directorySegments, ZipPath directoryPath) { Set coveredDimensions = new HashSet<>(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java index 3a037e2c..b308572d 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java @@ -30,13 +30,13 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Int32Value; import java.util.Collection; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.CheckReturnValue; /** A single parsed name of the assets directory path. */ @Immutable diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java index 7d96620e..3c29b871 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.MoreCollectors.toOptional; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.AssetsDirectoryTargeting; @@ -256,10 +257,18 @@ public static ImmutableSet extractTextureCompress public static ImmutableSet extractDeviceTiers( ImmutableSet targetedDirectories) { return targetedDirectories.stream() - .map(directory -> directory.getTargeting(TargetingDimension.DEVICE_TIER)) + .map(TargetingUtils::extractDeviceTier) .flatMap(Streams::stream) - .flatMap(targeting -> targeting.getDeviceTier().getValueList().stream()) - .map(Int32Value::getValue) .collect(toImmutableSet()); } + + public static Optional extractDeviceTier(TargetedDirectory targetedDirectory) { + return targetedDirectory + .getTargeting(TargetingDimension.DEVICE_TIER) + .flatMap( + targeting -> + targeting.getDeviceTier().getValueList().stream() + .map(Int32Value::getValue) + .collect(toOptional())); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java new file mode 100644 index 00000000..5b63bf40 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model.utils; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.ClassesDexNameSanitizer; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Utility class to help parse bundles */ +public class BundleParser { + + private BundleParser() {} + + public static final ZipPath METADATA_DIRECTORY = ZipPath.create("BUNDLE-METADATA"); + + public static final String BUNDLE_CONFIG_FILE_NAME = "BundleConfig.pb"; + + /** + * Returns the {@link BundleModuleName} corresponding to the provided zip entry. If the zip entry + * does not belong to a module, a null {@link BundleModuleName} is returned. + */ + public static Optional extractModuleName( + ZipEntry entry, ImmutableSet nonModuleDirectories) { + ZipPath path = ZipPath.create(entry.getName()); + + for (ZipPath nonModuleDirectory : nonModuleDirectories) { + if (path.startsWith(nonModuleDirectory)) { + return Optional.empty(); + } + } + + // Ignoring top-level files. + if (path.getNameCount() <= 1) { + return Optional.empty(); + } + + // Temporarily excluding .class files. + if (path.toString().endsWith(".class")) { + return Optional.empty(); + } + + return Optional.of(BundleModuleName.create(path.getName(0).toString())); + } + + /** Extracts all modules from bundle zip file and returns them in a list */ + public static ImmutableList extractModules( + ZipFile bundleFile, BundleConfig bundleConfig, ImmutableSet nonModuleDirectories) { + Map moduleBuilders = new HashMap<>(); + Enumeration entries = bundleFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + Optional moduleName = extractModuleName(entry, nonModuleDirectories); + if (!moduleName.isPresent()) { + continue; + } + + BundleModule.Builder moduleBuilder = + moduleBuilders.computeIfAbsent( + moduleName.get(), + name -> BundleModule.builder().setName(name).setBundleConfig(bundleConfig)); + + moduleBuilder.addEntry( + ModuleEntry.builder() + .setBundleLocation( + ModuleEntryBundleLocation.create( + Paths.get(bundleFile.getName()), ZipPath.create(entry.getName()))) + .setPath(ZipUtils.convertBundleToModulePath(ZipPath.create(entry.getName()))) + .setContent(ZipUtils.asByteSource(bundleFile, entry)) + .build()); + } + + // We verify the presence of the manifest before building the BundleModule objects because the + // manifest is a required field of the BundleModule class. + checkModulesHaveManifest(moduleBuilders.values()); + + return moduleBuilders.values().stream() + .map(BundleModule.Builder::build) + .collect(toImmutableList()); + } + + private static void checkModulesHaveManifest(Collection bundleModules) { + ImmutableSet modulesWithoutManifest = + bundleModules.stream() + .filter(bundleModule -> !bundleModule.hasAndroidManifest()) + .map(module -> module.getName().getName()) + .collect(toImmutableSet()); + if (!modulesWithoutManifest.isEmpty()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found modules in the App Bundle without an AndroidManifest.xml: %s", + modulesWithoutManifest) + .build(); + } + } + + /** Loads BundleConfig.pb from zip file into {@link BundleConfig} */ + public static BundleConfig readBundleConfig(ZipFile bundleFile) { + ZipEntry bundleConfigEntry = bundleFile.getEntry(BUNDLE_CONFIG_FILE_NAME); + if (bundleConfigEntry == null) { + throw InvalidBundleException.builder() + .withUserMessage("File '%s' was not found.", BUNDLE_CONFIG_FILE_NAME) + .build(); + } + + try { + return BundleConfig.parseFrom(ZipUtils.asByteSource(bundleFile, bundleConfigEntry).read()); + } catch (InvalidProtocolBufferException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage("Bundle config '%s' could not be parsed.", BUNDLE_CONFIG_FILE_NAME) + .build(); + } catch (IOException e) { + throw new UncheckedIOException( + String.format("Error reading file '%s'.", BUNDLE_CONFIG_FILE_NAME), e); + } + } + + /** Loads BUNDLE-METADATA into {@link BundleMetadata} */ + public static BundleMetadata readBundleMetadata(ZipFile bundleFile) { + BundleMetadata.Builder metadata = BundleMetadata.builder(); + ZipUtils.allFileEntries(bundleFile) + .filter(entry -> ZipPath.create(entry.getName()).startsWith(METADATA_DIRECTORY)) + .forEach( + zipEntry -> { + ZipPath bundlePath = ZipPath.create(zipEntry.getName()); + // Strip the top-level metadata directory. + ZipPath metadataPath = bundlePath.subpath(1, bundlePath.getNameCount()); + metadata.addFile(metadataPath, ZipUtils.asByteSource(bundleFile, zipEntry)); + }); + return metadata.build(); + } + + /** + * Renames classes1.dex files to classes.dex in the given modules. This is a temporary fix to work + * around a bug in gradle that creates a file named classes1.dex + */ + @CheckReturnValue + public static ImmutableList sanitize(ImmutableList modules) { + modules = + modules.stream().map(new ClassesDexNameSanitizer()::sanitize).collect(toImmutableList()); + + return modules; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java b/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java new file mode 100644 index 00000000..344b8b96 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model.utils; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Entry; +import com.android.aapt.Resources.FileReference; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Value; +import com.android.aapt.Resources.XmlNode; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ResourceId; +import com.android.tools.build.bundletool.model.ResourceInjector; +import com.android.tools.build.bundletool.model.VariantKey; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteSource; + +/** + * Adds a localeConfig attribute within in Android Manifest and injects a + * locales_config.xml resource to the master base module when splitting APKs by language. + * + *

    The format of the localeConfig attribute is as the following: ... + * + *

    It is done only for split and system APKs, not standalone and instant APKs. + */ +public class LocaleConfigXmlInjector { + private static final String XML_TYPE_NAME = "xml"; + private static final String RESOURCE_PATH = "res/xml/locales_config.xml"; + private static final String RESOURCE_FILE_NAME = "locales_config"; + private static final String LOCALE_CONFIG_ELEMENT = "locale-config"; + private static final String LOCALE_ELEMENT = "locale"; + private static final String LOCALE_ATTRIBUTE_NAME = "android:name"; + + public ImmutableList process( + VariantKey variantKey, ImmutableList splits) { + switch (variantKey.getSplitType()) { + case SYSTEM: + // Injection for system APK variant is same as split APK variant as both + // contain single base-master split which is always installed and additional + // splits. In case of system APK variant base-master split is the fused system + // split and splits are unmatched language splits. + case SPLIT: + return processSplitApkVariant(splits); + // No need to inject a locales_config.xml if resources with all languages are + // available inside the one APK. + case STANDALONE: + case INSTANT: + case ARCHIVE: + return splits; + case ASSET_SLICE: + throw new IllegalStateException("Unexpected Asset Slice inside variant."); + } + throw new IllegalStateException( + String.format("Unknown split type %s", variantKey.getSplitType())); + } + + private static ImmutableList processSplitApkVariant( + ImmutableList splits) { + // Only inject a locales_config.xml to the base module splits + boolean hasLanguageSplits = + splits.stream() + .anyMatch( + split -> + split.isBaseModuleSplit() && split.getApkTargeting().hasLanguageTargeting()); + + XmlNode localesXmlContent = createLocalesXmlNode(splits); + + ImmutableList.Builder result = new ImmutableList.Builder<>(); + + for (ModuleSplit split : splits) { + // Inject a locales_config.xml when all of the following conditions are met: + // 1. It's a master split + // 2. It's a base module split + // 3. It has the resource table + // 4. The localeConfig key was not added in the APP’s Manifest. + // 5. The language split configuration was enabled. + // 6. The locales_config.xml doesn't exist in the resources(res/xml/). + if (split.isMasterSplit() + && split.isBaseModuleSplit() + && split.getResourceTable().isPresent() + && hasLanguageSplits + && !split.getAndroidManifest().hasLocaleConfig() + && !split.findEntry(ZipPath.create(RESOURCE_PATH)).isPresent()) { + result.add(injectLocaleConfigXml(split, localesXmlContent)); + } else { + result.add(split); + } + } + return result.build(); + } + + private static ModuleSplit injectLocaleConfigXml(ModuleSplit split, XmlNode xmlNode) { + + ResourceInjector resourceInjector = ResourceInjector.fromModuleSplit(split); + ResourceId resourceId = resourceInjector.addResource(XML_TYPE_NAME, createXmlEntry()); + + ModuleEntry localesConfigEntry = addLocalesConfigEntry(xmlNode); + + return split.toBuilder() + .setResourceTable(resourceInjector.build()) + // Inject a locales_config.xml + .setEntries( + ImmutableList.builder() + .addAll(split.getEntries()) + .add(localesConfigEntry) + .build()) + // Add a localeConfig key to the Manifest pointing to an XML file where the locales + // configuration were stored. + .setAndroidManifest( + split + .getAndroidManifest() + .toEditor() + .setLocaleConfig(resourceId.getFullResourceId()) + .save()) + .build(); + } + + private static ModuleEntry addLocalesConfigEntry(XmlNode xmlNode) { + return ModuleEntry.builder() + .setPath(ZipPath.create(RESOURCE_PATH)) + .setContent(ByteSource.wrap(xmlNode.toByteArray())) + .build(); + } + + public static XmlNode createLocalesXmlNode(ImmutableList splits) { + // Get all locales from the base module splits + ImmutableSet allLocales = getLocalesFromBaseModuleSplits(splits); + + XmlProtoElementBuilder localesConfigXml = XmlProtoElementBuilder.create(LOCALE_CONFIG_ELEMENT); + + allLocales.stream() + .filter(locale -> !locale.isEmpty()) + .forEach(locale -> localesConfigXml.addChildElement(createAttributes(locale))); + + return XmlProtoNode.createElementNode(localesConfigXml.build()).getProto(); + } + + private static ImmutableSet getLocalesFromBaseModuleSplits( + ImmutableList splits) { + // TODO("b/213543969"): Bundletool is infeasible to detect the set of languages a developer + // explicitly supports if the app has dependencies which themselves provide extra resources + // from libraries such as Android X or Jetpack in many languages. The bug is created for the + // gradle to add a separate file with the resource source information, rather than including it + // in the ResourceTable. + return splits.stream() + // Only inject a locales_config.xml to the base module splits + .filter(ModuleSplit::isBaseModuleSplit) + .filter(split -> split.getResourceTable().isPresent()) + .flatMap(split -> ResourcesUtils.getAllLocales(split.getResourceTable().get()).stream()) + .filter(locale -> !locale.isEmpty()) + .collect(toImmutableSet()); + } + + private static XmlProtoElementBuilder createAttributes(String locale) { + return XmlProtoElementBuilder.create(LOCALE_ELEMENT) + .addAttribute( + XmlProtoAttributeBuilder.create(LOCALE_ATTRIBUTE_NAME).setValueAsString(locale)); + } + + private static Entry createXmlEntry() { + return Entry.newBuilder() + // Set filename without ".xml" as resource name. + .setName(RESOURCE_FILE_NAME) + .addConfigValue( + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setItem( + Item.newBuilder() + .setFile( + FileReference.newBuilder() + .setPath(RESOURCE_PATH) + .setType(FileReference.Type.PROTO_XML))))) + .build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java index 9c4da61c..aa23fcd3 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java @@ -234,6 +234,13 @@ public static ImmutableSet getAllLanguages(ResourceTable table) { .collect(toImmutableSet()); } + /** Returns all locales present in a given resource table. */ + public static ImmutableSet getAllLocales(ResourceTable table) { + return configValues(table) + .map(configValue -> configValue.getConfig().getLocale()) + .collect(toImmutableSet()); + } + /** Returns the smallest screen density from the ones given. */ public static DensityAlias getLowestDensity(ImmutableCollection densities) { return densities.stream().min(comparing(DENSITY_ALIAS_TO_DPI_MAP::get)).get(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java index c3c339bb..f9b22cc1 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java @@ -106,9 +106,9 @@ public static ImmutableList systemApkVariants(ImmutableList va return variants.stream().filter(ResultUtils::isSystemApkVariant).collect(toImmutableList()); } - public static ImmutableList hibernatedApkVariants(BuildApksResult result) { + public static ImmutableList archivedApkVariants(BuildApksResult result) { return result.getVariantList().stream() - .filter(ResultUtils::isHibernatedApkVariant) + .filter(ResultUtils::isArchivedApkVariant) .collect(toImmutableList()); } @@ -150,10 +150,10 @@ public static boolean isSystemApkVariant(Variant variant) { .anyMatch(ApkDescription::hasSystemApkMetadata); } - public static boolean isHibernatedApkVariant(Variant variant) { + public static boolean isArchivedApkVariant(Variant variant) { return variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) - .anyMatch(ApkDescription::hasHibernatedApkMetadata); + .anyMatch(ApkDescription::hasArchivedApkMetadata); } public static ImmutableSet getAllTargetedLanguages(BuildApksResult result) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java b/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java index 5f15d4b8..90093a01 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java @@ -24,12 +24,12 @@ import com.android.aapt.Resources.Item; import com.android.aapt.Resources.Value; import com.android.aapt.Resources.XmlNode; -import com.android.tools.build.bundletool.model.GeneratedApks; import com.android.tools.build.bundletool.model.ModuleEntry; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ResourceId; import com.android.tools.build.bundletool.model.ResourceInjector; import com.android.tools.build.bundletool.model.SplitsProtoXmlBuilder; +import com.android.tools.build.bundletool.model.VariantKey; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; @@ -47,36 +47,27 @@ public class SplitsXmlInjector { private static final String XML_PATH_PATTERN = "res/xml/splits%s.xml"; private static final String METADATA_KEY = "com.android.vending.splits"; - public GeneratedApks process(GeneratedApks generatedApks) { - // A variant is a set of APKs. One device is guaranteed to receive only APKs from the same. This - // is why we are processing split.xml for each variant separately. - return GeneratedApks.fromModuleSplits( - generatedApks.getAllApksGroupedByOrderedVariants().asMap().entrySet().stream() - .map( - keySplit -> { - switch (keySplit.getKey().getSplitType()) { - case SYSTEM: - // Injection for system APK variant is same as split APK variant as both - // contain single base-master split which is always installed and additional - // splits. In case of system APK variant base-master split is the fused system - // split and splits are unmatched language splits. - case SPLIT: - return processSplitApkVariant(keySplit.getValue()); - case STANDALONE: - case HIBERNATION: - return keySplit.getValue().stream() - .map(SplitsXmlInjector::processStandaloneVariant) - .collect(toImmutableList()); - case INSTANT: - return keySplit.getValue(); - case ASSET_SLICE: - throw new IllegalStateException("Unexpected Asset Slice inside variant."); - } - throw new IllegalStateException( - String.format("Unknown split type %s", keySplit.getKey().getSplitType())); - }) - .flatMap(Collection::stream) - .collect(toImmutableList())); + public ImmutableList process(VariantKey variantKey, Collection splits) { + switch (variantKey.getSplitType()) { + case SYSTEM: + // Injection for system APK variant is same as split APK variant as both + // contain single base-master split which is always installed and additional + // splits. In case of system APK variant base-master split is the fused system + // split and splits are unmatched language splits. + case SPLIT: + return processSplitApkVariant(splits); + case STANDALONE: + return splits.stream() + .map(SplitsXmlInjector::processStandaloneVariant) + .collect(toImmutableList()); + case INSTANT: + case ARCHIVE: + return ImmutableList.copyOf(splits); + case ASSET_SLICE: + throw new IllegalStateException("Unexpected Asset Slice inside variant."); + } + throw new IllegalStateException( + String.format("Unknown split type %s", variantKey.getSplitType())); } private static ModuleSplit processStandaloneVariant(ModuleSplit split) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java index 472f4fe4..698eb7da 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "1.8.2"; + private static final String CURRENT_VERSION = "1.9.0"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java index a4690982..e1236847 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java @@ -93,7 +93,10 @@ public enum VersionGuardedFeature { * Install time modules will be merged into base unless explicitly turned off via in "install-time" attribute. */ - MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"); + MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"), + + /* Enabling generation of archived apk. */ + ARCHIVED_APK_GENERATION("1.8.2"); /** Version from which the given feature should be enabled by default. */ private final Version enabledSinceVersion; @@ -128,4 +131,8 @@ public boolean enabledForVersion(Version bundletoolVersion) { return disabledSinceVersion.map(bundletoolVersion::isOlderThan).orElse(true); } + + public Version getEnabledSinceVersion() { + return enabledSinceVersion; + } } diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java index 24d01c13..b3f66e82 100644 --- a/src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java @@ -28,8 +28,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.Set; -import javax.annotation.CheckReturnValue; import javax.inject.Inject; /** Identify embedded APKs which should be signed with the same key as generated APKs. */ diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java index 892ed094..47244f70 100644 --- a/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java @@ -23,7 +23,7 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; -import javax.annotation.CheckReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; import javax.inject.Inject; /** diff --git a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java index 77620153..0275438b 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java @@ -102,7 +102,8 @@ public ImmutableList generateStandaloneApks( .collect(toImmutableList()); } - private static ModuleSplit setVariantTargetingAndSplitType(ModuleSplit shard) { + /** Sets the variant targeting and split type to standalone. */ + public static ModuleSplit setVariantTargetingAndSplitType(ModuleSplit shard) { return shard.toBuilder() .setVariantTargeting(standaloneApkVariantTargeting(shard)) .setSplitType(SplitType.STANDALONE) diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java index 84d6ae29..922e51d0 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java @@ -28,7 +28,9 @@ public class AppBundleValidator { @VisibleForTesting static final ImmutableList DEFAULT_BUNDLE_FILE_SUB_VALIDATORS = // Keep order of common validators in sync with BundleModulesValidator. - ImmutableList.of(new BundleZipValidator(), new MandatoryFilesPresenceValidator()); + ImmutableList.of( + new BundleZipValidator(), + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES)); /** Validators run on the internal representation of bundle and bundle modules. */ @VisibleForTesting diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java index 0a62a553..a2318fa8 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java @@ -20,6 +20,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -43,7 +44,7 @@ public class BundleModulesValidator { @VisibleForTesting static final ImmutableList MODULE_FILE_SUB_VALIDATORS = // Keep order of common validators in sync with AppBundleValidator. - ImmutableList.of(new MandatoryFilesPresenceValidator()); + ImmutableList.of(new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES)); /** Validators run on the internal representation of bundle modules. */ @VisibleForTesting diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java index 10a682d2..882d30d8 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java @@ -28,7 +28,7 @@ public void validateBundleZipEntry(ZipFile bundleFile, ZipEntry zipEntry) { if (zipEntry.isDirectory()) { throw InvalidBundleException.builder() .withUserMessage( - "The App Bundle zip file contains directory zip entry '%s' which is not allowed.", + "The bundle zip file contains directory zip entry '%s' which is not allowed.", zipEntry.getName()) .build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidator.java index fc1a3ed2..097731db 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidator.java @@ -17,12 +17,17 @@ package com.android.tools.build.bundletool.validation; import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractAssetsTargetedDirectories; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractDeviceTier; import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractDeviceTiers; +import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; +import static java.util.Comparator.naturalOrder; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.targeting.TargetingDimension; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; /** Validates that all modules that use device tier targeting contain the same set of tiers. */ public class DeviceTierParityValidator extends SubValidator { @@ -39,6 +44,9 @@ public void validateAllModules(ImmutableList modules) { continue; } + validateContiguousTiers(module, moduleTiers); + validateModuleDirectoryParity(module, moduleTiers); + if (referenceTiers == null) { referenceModule = module; referenceTiers = moduleTiers; @@ -52,4 +60,42 @@ public void validateAllModules(ImmutableList modules) { } } } + + /** Validates that tiers used by a module are contiguous and start from 0. */ + private void validateContiguousTiers(BundleModule module, ImmutableSet moduleTiers) { + int minTier = moduleTiers.stream().min(naturalOrder()).get(); + int maxTier = moduleTiers.stream().max(naturalOrder()).get(); + + if (minTier != 0 || maxTier != moduleTiers.size() - 1) { + throw InvalidBundleException.builder() + .withUserMessage( + "All modules with device tier targeting must support the same contiguous" + + " range of tier values starting from 0, but module '%s' supports %s.", + module.getName(), moduleTiers) + .build(); + } + } + + /** Validates that all targeted directories in a module have the same set of tiers. */ + private void validateModuleDirectoryParity( + BundleModule module, ImmutableSet moduleTiers) { + ImmutableSetMultimap tiersPerDirectory = + extractAssetsTargetedDirectories(module).stream() + .filter(directory -> extractDeviceTier(directory).isPresent()) + .collect( + toImmutableSetMultimap( + directory -> directory.getSubPathBaseName(TargetingDimension.DEVICE_TIER), + directory -> extractDeviceTier(directory).get())); + + for (String subpath : tiersPerDirectory.keySet()) { + if (!tiersPerDirectory.get(subpath).equals(moduleTiers)) { + throw InvalidBundleException.builder() + .withUserMessage( + "All device-tier-targeted folders in a module must support the same set of" + + " tiers, but module '%s' supports %s and folder '%s' supports only %s.", + module.getName(), moduleTiers, subpath, tiersPerDirectory.get(subpath)) + .build(); + } + } + } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidator.java index f37e4f87..c545fc06 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidator.java @@ -32,8 +32,11 @@ /** Validates presence of mandatory bundle/module files. */ public class MandatoryFilesPresenceValidator extends SubValidator { - private static final ImmutableSet NON_MODULE_DIRECTORIES = - ImmutableSet.of(AppBundle.METADATA_DIRECTORY, ZipPath.create("META-INF")); + private final ImmutableSet nonModuleDirectories; + + MandatoryFilesPresenceValidator(ImmutableSet nonModuleDirectories) { + this.nonModuleDirectories = nonModuleDirectories; + } @Override public void validateModuleZipFile(ZipFile moduleFile) { @@ -51,7 +54,7 @@ public void validateBundleZipFile(ZipFile bundleFile) { .map(ZipPath::create) .filter(entryPath -> entryPath.getNameCount() > 1) .map(entryPath -> entryPath.getName(0)) - .filter(not(NON_MODULE_DIRECTORIES::contains)) + .filter(not(nonModuleDirectories::contains)) .collect(toImmutableSet()); checkBundleHasBundleConfig(bundleFile); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java new file mode 100644 index 00000000..6607e636 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.model.AndroidManifest.ACTIVITY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_LOCATION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_TREE_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.RECEIVER_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_MAJOR_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_ATTRIBUTE_NAME; + +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; +import com.google.common.collect.ImmutableList; + +/** Validates {@code AndroidManifest.xml} file for the SDK module. */ +public class SdkAndroidManifestValidator extends SubValidator { + + @Override + public void validateModule(BundleModule module) { + AndroidManifest manifest = module.getAndroidManifest(); + validateSingleSdkLibrary(manifest); + validateVersioningTagsSet(manifest); + validateInternalOnlyIfInstallLocationSet(manifest); + validateNoPermissions(manifest); + validateNoSharedUserId(manifest); + validateNoComponents(manifest); + validateNoSplitId(manifest); + } + + private void validateSingleSdkLibrary(AndroidManifest manifest) { + ImmutableList sdkLibraryTags = manifest.getSdkLibraryElements(); + if (sdkLibraryTags.size() != 1) { + throw InvalidBundleException.builder() + .withUserMessage( + "There must be exactly one <%s> element. %d were found.", + SDK_LIBRARY_ELEMENT_NAME, sdkLibraryTags.size()) + .build(); + } + } + + private void validateVersioningTagsSet(AndroidManifest manifest) { + XmlProtoElement sdkLibraryTag = manifest.getSdkLibraryElements().get(0); + if (!sdkLibraryTag + .getAttribute(ANDROID_NAMESPACE_URI, SDK_MAJOR_VERSION_ATTRIBUTE_NAME) + .isPresent()) { + throw InvalidBundleException.builder() + .withUserMessage( + "SDK Major version must be set within the <%s> element.", SDK_LIBRARY_ELEMENT_NAME) + .build(); + } + + try { + Long.parseLong( + sdkLibraryTag + .getAttribute(ANDROID_NAMESPACE_URI, SDK_MAJOR_VERSION_ATTRIBUTE_NAME) + .get() + .getValueAsString()); + } catch (NumberFormatException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage( + "SDK Major version in <%s> cannot be parsed to a Long.", SDK_LIBRARY_ELEMENT_NAME) + .build(); + } + + if (manifest.getMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME).isPresent()) { + try { + Long.parseLong(manifest.getMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME).get()); + } catch (NumberFormatException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage( + "SDK Patch version in <%s> cannot be parsed to a Long.", META_DATA_ELEMENT_NAME) + .build(); + } + } + } + + private void validateInternalOnlyIfInstallLocationSet(AndroidManifest manifest) { + if (manifest.getInstallLocationValue().isPresent() + && !manifest.getInstallLocationValue().get().equals("internalOnly")) { + throw InvalidBundleException.builder() + .withUserMessage( + "'%s' in must be 'internalOnly' for SDK bundles if it is set.", + INSTALL_LOCATION_ATTRIBUTE_NAME) + .build(); + } + } + + private void validateNoPermissions(AndroidManifest manifest) { + if (!manifest.getPermissions().isEmpty()) { + throw InvalidBundleException.builder() + .withUserMessage( + "<%s> cannot be declared in the manifest of an SDK bundle.", PERMISSION_ELEMENT_NAME) + .build(); + } + + if (!manifest.getPermissionGroups().isEmpty()) { + throw InvalidBundleException.builder() + .withUserMessage( + "<%s> cannot be declared in the manifest of an SDK bundle.", + PERMISSION_GROUP_ELEMENT_NAME) + .build(); + } + + if (!manifest.getPermissionTrees().isEmpty()) { + throw InvalidBundleException.builder() + .withUserMessage( + "<%s> cannot be declared in the manifest of an SDK bundle.", + PERMISSION_TREE_ELEMENT_NAME) + .build(); + } + } + + private void validateNoSharedUserId(AndroidManifest manifest) { + if (manifest.hasSharedUserId()) { + throw InvalidBundleException.builder() + .withUserMessage( + "'%s' attribute cannot be used in the manifest of an SDK bundle.", + SHARED_USER_ID_ATTRIBUTE_NAME) + .build(); + } + } + + private void validateNoComponents(AndroidManifest manifest) { + if (manifest.hasComponents()) { + throw InvalidBundleException.builder() + .withUserMessage( + "None of <%s>, <%s>, <%s>, or <%s> can be declared in the manifest of an SDK bundle.", + ACTIVITY_ELEMENT_NAME, + SERVICE_ELEMENT_NAME, + PROVIDER_ELEMENT_NAME, + RECEIVER_ELEMENT_NAME) + .build(); + } + } + + private void validateNoSplitId(AndroidManifest manifest) { + if (manifest.getSplitId().isPresent()) { + throw InvalidBundleException.builder() + .withUserMessage("'split' attribute cannot be used in the manifest of an SDK bundle.") + .build(); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidator.java new file mode 100644 index 00000000..cee95297 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidator.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Validates that an SDK bundle has only one module. */ +public class SdkBundleHasOneModuleValidator extends SubValidator { + + @Override + public void validateBundleZipFile(ZipFile bundleFile) { + checkSdkHasSingleModule(bundleFile); + } + + private void checkSdkHasSingleModule(ZipFile bundleFile) { + ImmutableSet moduleDirectories = getModuleDirectories(bundleFile); + if (moduleDirectories.size() != 1) { + throw InvalidBundleException.builder() + .withUserMessage( + "SDK bundles need exactly one module, %d detected.", moduleDirectories.size()) + .build(); + } + } + + private ImmutableSet getModuleDirectories(ZipFile bundleFile) { + return Collections.list(bundleFile.entries()).stream() + .map(ZipEntry::getName) + .map(ZipPath::create) + .filter(entryPath -> entryPath.getNameCount() > 1) + .map(entryPath -> entryPath.getName(0)) + .filter(not(SdkBundle.NON_MODULE_DIRECTORIES::contains)) + .collect(toImmutableSet()); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidator.java new file mode 100644 index 00000000..2055493b --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.model.BundleModuleName.BASE_MODULE_NAME; + +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; + +/** Validator ensuring that the module inside an SDK bundle is named correctly. */ +final class SdkBundleModuleNameValidator extends SubValidator { + + @Override + public void validateModule(BundleModule module) { + if (!module.getName().equals(BASE_MODULE_NAME)) { + throw InvalidBundleException.builder() + .withUserMessage("The SDK bundle module must be named '%s'.", BASE_MODULE_NAME) + .build(); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleValidator.java new file mode 100644 index 00000000..4984425a --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkBundleValidator.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import com.android.tools.build.bundletool.model.SdkBundle; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.util.zip.ZipFile; + +/** Validates the files and configuration for the SDK bundle. */ +public class SdkBundleValidator { + + /** Validators run on the bundle zip file. */ + @VisibleForTesting + static final ImmutableList DEFAULT_BUNDLE_FILE_SUB_VALIDATORS = + // Keep order of common validators in sync with BundleModulesValidator. + ImmutableList.of( + new BundleZipValidator(), + new MandatoryFilesPresenceValidator(SdkBundle.NON_MODULE_DIRECTORIES), + new SdkBundleHasOneModuleValidator()); + + /** Validators run on the internal representation of bundle and bundle modules. */ + @VisibleForTesting + static final ImmutableList DEFAULT_BUNDLE_SUB_VALIDATORS = + // Keep order of common validators in sync with BundleModulesValidator. + ImmutableList.of( + // Fundamental file validations first. + new BundleFilesValidator(), + new SdkBundleModuleNameValidator(), + // More specific file validations. + new DexFilesValidator(), + new SdkAndroidManifestValidator(), + // Other. + new ResourceTableValidator()); + + private final ImmutableList allBundleFileSubValidators; + private final ImmutableList allBundleSubValidators; + + private SdkBundleValidator( + ImmutableList allBundleSubValidators, + ImmutableList allBundleFileSubValidators) { + this.allBundleSubValidators = allBundleSubValidators; + this.allBundleFileSubValidators = allBundleFileSubValidators; + } + + public static SdkBundleValidator create() { + return create(ImmutableList.of()); + } + + public static SdkBundleValidator create(ImmutableList extraSubValidators) { + return new SdkBundleValidator( + ImmutableList.builder() + .addAll(DEFAULT_BUNDLE_SUB_VALIDATORS) + .addAll(extraSubValidators) + .build(), + ImmutableList.builder() + .addAll(DEFAULT_BUNDLE_FILE_SUB_VALIDATORS) + .addAll(extraSubValidators) + .build()); + } + + /** Validates the given Sdk Bundle zip file. */ + public void validateFile(ZipFile bundleFile) { + new ValidatorRunner(allBundleFileSubValidators).validateBundleZipFile(bundleFile); + } + + /** Validates the given Sdk Bundle. */ + public void validate(SdkBundle bundle) { + new ValidatorRunner(allBundleSubValidators).validateSdkBundle(bundle); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java index 92eb9082..78b367c3 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java @@ -18,13 +18,14 @@ import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** - * Validates a particular property of the {@link AppBundle}. + * Validates a particular property of the bundle. * *

    Sub-classes override some of the methods that is/are most convenient for the particular type * of validation. @@ -41,10 +42,14 @@ public void validateBundleZipFile(ZipFile bundleFile) {} public void validateBundleZipEntry(ZipFile bundleFile, ZipEntry zipEntry) {} - // Validations of the AppBundle object and it's internals. + // Validations of the AppBundle object and its internals. + /** Validates an AppBundle object. */ public void validateBundle(AppBundle bundle) {} + /** Validates an SdkBundle object. */ + public void validateSdkBundle(SdkBundle bundle) {} + public void validateAllModules(ImmutableList modules) {} public void validateModule(BundleModule module) {} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java b/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java index 4f645f36..d8d329a3 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java @@ -21,6 +21,7 @@ import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import java.util.Enumeration; @@ -58,6 +59,11 @@ public void validateBundle(AppBundle bundle) { subValidators.forEach(subValidator -> validateBundleUsingSubValidator(bundle, subValidator)); } + /** Validates the given SDK Bundle. */ + void validateSdkBundle(SdkBundle bundle) { + subValidators.forEach(subValidator -> validateSdkBundleUsingSubValidator(bundle, subValidator)); + } + /** Interprets given modules as a bundle and validates it. */ public void validateBundleModules(ImmutableList modules) { subValidators.forEach( @@ -70,6 +76,18 @@ private static void validateBundleUsingSubValidator(AppBundle bundle, SubValidat ImmutableList.copyOf(bundle.getModules().values()), subValidator); } + private static void validateSdkBundleUsingSubValidator( + SdkBundle bundle, SubValidator subValidator) { + subValidator.validateSdkBundle(bundle); + + BundleModule module = bundle.getModule(); + subValidator.validateModule(module); + + for (ZipPath moduleFile : getModuleFiles(module)) { + subValidator.validateModuleFile(moduleFile); + } + } + private static void validateBundleModulesUsingSubValidator( ImmutableList modules, SubValidator subValidator) { subValidator.validateAllModules(modules); diff --git a/src/main/proto/app_integrity_config.proto b/src/main/proto/app_integrity_config.proto index 26b457e9..51fcb045 100644 --- a/src/main/proto/app_integrity_config.proto +++ b/src/main/proto/app_integrity_config.proto @@ -6,7 +6,7 @@ option java_package = "com.android.bundle"; // Specifies integrity protection options that should be applied to an app // bundle. -// Next tag: 7. +// Next tag: 8. message AppIntegrityConfig { bool enabled = 1; LicenseCheck license_check = 2; @@ -16,6 +16,7 @@ message AppIntegrityConfig { // Optional. If present, java/kotlin code will be obfuscated according to the // config. DexProtectionConfig dex_protection_config = 6; + string version_label = 7; } // Next tag: 4. @@ -35,6 +36,7 @@ message InstallerCheck { // Next tag: 2 message DebuggerCheck { option deprecated = true; + bool enabled = 1; } diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index b37e3061..f1f2baa4 100644 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -36,6 +36,35 @@ message BuildApksResult { repeated PermanentlyFusedModule permanently_fused_modules = 8; } +message BuildSdkApksResult { + // The package name of the SDK. + // + // For instance, for SDK “com.foo.bar” with major version “15”, + // the package name stored here is simply “com.foo.bar”. + // This is different from the package name that is installed in Android + // PackageManager on sandbox-enabled devices (which is “com.foo.bar_15”). + string package_name = 1; + + // Variants generated for the SDK. + // At the moment, there is always a single variant. + repeated Variant variant = 2; + + Bundletool bundletool = 3; + + SdkVersionInformation version = 4; +} + +message SdkVersionInformation { + // Major version of the SDK. + uint64 major = 1; + + // Patch version of the SDK. + uint64 patch = 2; + + // A unique version code assigned to the SDK by the caller of build-sdk-apks. + uint32 version_code = 3; +} + // Variant is a group of APKs that covers a part of the device configuration // space. APKs from multiple variants are never combined on one device. message Variant { @@ -181,8 +210,8 @@ message ApkDescription { SplitApkMetadata asset_slice_metadata = 7; // Set only for APEX APKs. ApexApkMetadata apex_apk_metadata = 8; - // Set only for hibernated APKs. - HibernatedApkMetadata hibernated_apk_metadata = 9; + // Set only for archived APKs. + ArchivedApkMetadata archived_apk_metadata = 9; } } @@ -216,8 +245,8 @@ message ApexApkMetadata { repeated ApexEmbeddedApkConfig apex_embedded_apk_config = 1; } -// Holds data specific to Hibernated APKs. -message HibernatedApkMetadata {} +// Holds data specific to Archived APKs. +message ArchivedApkMetadata {} message LocalTestingInfo { // Indicates if the bundle is built in local testing mode. diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index 425e88de..df7c60bc 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -60,6 +60,23 @@ message Compression { // asset modules; the content of on-demand and fast-follow asset modules is // always kept uncompressed. AssetModuleCompression install_time_asset_module_default_compression = 2; + + enum ApkCompressionAlgorithm { + // Default in the current version of bundletool is zlib deflate algorithm + // with compression level 9 for the application's resources and compression + // level 6 for other entries. + // + // This is a good trade-off between size of final APK and size of patches + // which are used to update the application from previous to next version. + DEFAULT_APK_COMPRESSION_ALGORITHM = 0; + + // 7zip implementation of deflate algorithm which gives smaller APK size + // but size of patches required to update the application are larger. + P7ZIP = 1; + } + + // Compression algorithm which is used to compress entries in final APKs. + ApkCompressionAlgorithm apk_compression_algorithm = 3; } // Resources to keep in the master split. diff --git a/src/main/proto/rotation_config.proto b/src/main/proto/rotation_config.proto index 267677d9..6521a82d 100644 --- a/src/main/proto/rotation_config.proto +++ b/src/main/proto/rotation_config.proto @@ -6,11 +6,16 @@ option java_package = "com.android.bundle"; // Specifies the config that gets applied to the rotation aspect of the signing // process of the App Bundle. -// Next tag: 2 +// Next tag: 3 message RotationConfig { // The SHA256 fingerprint of the expected certificate to sign the APKs // generated from the Bundle. // Example: // FE:C0:E6:5B:F3:76:5D:A1:C2:56:13:C7:A3:60:35:A9:26:BC:3B:3A:39:9B:C8:55:40:F1:6D:55:17:3F:F5:9B string signing_certificate_sha256_fingerprint = 1; + + // The minimum API level for which an APK's rotated signing key should be used + // to produce the APK's signature. The original signing key for the APK will + // be used for all previous platform versions. + int32 rotation_min_sdk_version = 2; } diff --git a/src/main/proto/runtime_enabled_sdk_config.proto b/src/main/proto/runtime_enabled_sdk_config.proto new file mode 100644 index 00000000..8e53198b --- /dev/null +++ b/src/main/proto/runtime_enabled_sdk_config.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package android.bundle; + +option java_package = "com.android.bundle"; +option java_outer_classname = "RuntimeEnabledSdkConfigProto"; + +// Information about runtime-enabled SDK dependencies of a specific module +// inside the App Bundle. +message RuntimeEnabledSdkConfig { + // Runtime-enabled SDKs in this config. + repeated RuntimeEnabledSdk runtime_enabled_sdk = 1; +} + +message RuntimeEnabledSdk { + // Package name of the runtime-enabled SDK. + string package_name = 1; + // Major version of the runtime-enabled SDK. + uint64 version_major = 2; + // SHA-256 hash of the runtime-enabled SDK's signing certificate, represented + // as a string of bytes in hexadecimal form, with ':' separating the bytes. + string certificate_digest = 3; +} diff --git a/src/main/resources/com/android/tools/build/bundletool/archive/dex/classes.dex b/src/main/resources/com/android/tools/build/bundletool/archive/dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..316809e67ca577376de2b0e08025298598a6cd83 GIT binary patch literal 6460 zcmbuEe{5UT701te&vp{maU3U&6E|%gH-(fYanm;G4+pyZYB6=1kS1ZI9qF_E;=I;* z&wcNuq!ii?BvzqG0}U}z_6KO2Knygb{V}Ei+Qj}rV`I|L1RK)^Lz)KFrb+vyjs5tZ z>*v%-N;_#U`sAE@@44sxI`>{bJEtxAckJq=JOA{@$MWxQ%WeNb`_nJfZ#Xja=-f?x z#`)L#TZoE8v^2dt3?ZIEGtooPI6n^Vmx(q&dK98s$lEt^6^{5X9!98Fe+y@>8Pk~p#yWqFr3ivblCr~h7LC^#?fUTed^niWf05}B3 zzzmoJ_kk~f%iwwN9q<)^ZK``{RpY9c%?Rftx`G*amI^+d&W53-*H~K)4Y?m>QuV zT!h{V9U&t0cIbZS2ygMQeGTY@Qr367Hf+ZaLQ>S}ck?;T58HDaWqTeMKb+5F8+Pq^ zeEhI2>uKb3zj@63>;|k)BEQ$wPe9+}>hsY1T)kLfvs96Pu_FIch0cBB`Qf?cXCGkw zQRJ8Pa{pt<=f@9=F~}f#%k#t%+aGeXI0JYtce?qzAbH)7x$-z9$H0s$KL^S2Z~!#G zPq&bp#d#yaBd-<90;G(ZoR-n*At7b5g(O-OQlc}iTypczLMo_#AEejy=feo@Lb}FH z*C6`XZw*#*Qb;dZLVCz?)AvI9v2r=c07f|tS&JG5WDtLnE7?z->o?@edRK;B*|01X z`V8zjJt1U(riAnf{|(qRpLO%^7BWH^A?rvNGE66hEX@!5SrF+4Iwxc!-4EI5w%3TA zc85sU(<#Un?6{PWQOZI_kz+z`zzEMnwz~B;VponMy%8fm3d!rsYsPz%A8$#}$JoR+ zHZiYvW%+;bTtU7UJOIjPK)F7be}eo5a0zhT)%63-*!L*=GT^ggb$KhwyHLgtpDVl` zOaO7*3E7GAJt%(?@R`G9<$mvgo<_Oq)-Tt+gbmJnKS~$qbciF?31`k7Bq; z!{IIQHuPu{dJs@X6c9ZFYT48;Ij+0t0?3#LfmRVs>k3w zq7M>1gFf(@g~OFEPQ9WhMBjFO?Ne$g96BBfV2rJ(#a?-xI?(%#Nb~x5 z&`<8qYp8>;MBmwdJ8E)Y*s8-@4BaGb*dCoGjzj(o!!?;7p)Q}hW{Af!6${{`Y-sxc65*|b;z|+P~2ziOOiaTyrw)S@4@djNcIzzZo|6P zuJC{rtlv`KhJLnT90573jLMP{l{K-3Va(p?P$PWtEQU}M4y?^r#0t^TQ9KHM9W-aMmF#y@jxbVCg1X7KD2(t1R1 z#@~;2gVZRE1`jKAu&qm1l!@qmuX3QDg7TqE@O4}Y92>mWEcq_xacn>ir}_S4$+5zc z?_@4#$!T8sQo0S}Dm@&3T!xRoqRfYJvkial;e0OVblI-O&2J^8r+Wug_h$>bp*y#c zufHUN1LNcA)X>0WYV62tYMA_kQ>pau?Bw0!BNQ53)C;*G)0orq-6z#EDtU(rrmc~8 zcw}(u5J@R&NEx=H77D7Pn?`I-)eBmVq%_ItG%4wHivMwHhMLlX$s}moSN0z$BdzZo;{V;ZS;k` zT^(K7+o~!_YF^`&{TXPleN9PmAu&)aq6snIDPzt|(!Oh1jjP#HYF-;v4g9y-kEjdU z|1=?JU}e=L)i3)uG$%1-=}GEHXU&B~-Zb+CEwQ|;R(2j!n5YPvB;9;H%Zc`!rQWM% z3R;r3Uf*y*FD|IX8=BdUr5pJqZMv?vBqgp}uBjZIs-s#14i!|}=B}=_Nf`(px4i8d<)hj{#x>C3qWzjC&b*l$S1qhO zq7zoZG;iUAZYB<5mnNy9lrE@7J~3#T1x>{kX|Bi|npdr(+UZ5j$ih5aQ8Jb}sbvwM zm3c?qsgF#onZDB5Q8S_GrNe^4M<>T7MrOwch7J!L8ks#ZFgik$lk-{(M?-9@9n!up%4gru*zm~Jl<1OYI+eoVHI1Wenvb$+9A(lB$unuHJGOc@-3&3E<@s5s zX0w`YA1tVOo3^`Ek@57v(8w@FuWdS=8o7(AF-NloR+T~- zKAuM`%d|>|3i&gOj$;~=XNwv&V2gH_{hZN^972v_oLSnp%Vnjt7?>EkJvBW-(Q8<^ zD?rum8D>+J(3TwXWi1V{Pu`rCSot>QGHyb9f_~2KiYn7Wp)za7nK`3uztlhSco5#tqja zvGOdV;MFYQW1XIzEvSnIYFN}%k(kZk{LEW?f2_<$pENhWzS6#ATBYX=804thReF}S zo#ZR6JB4)nCB!^W;B|nr=)JHQE zf__9>bS$+nC?Zvqx{E~hC>_Vqh_hF7i>r|}sXOrJvFFWusTLnLED;U-;enb~Bryti z?7q@HK-G>}I+n=eSc{0#Gim{1>iqfPz31YH^rBw+-zMvajYEh5x@9FD|{pU) z_sr$?^ksW*FFDTg?FP%}NMx@Ttw^DnQLE=tW|xk>K#wB#eB zWo53cJRnD4(W*4pHHVt(zc{7N7@}VU1;6=}eB2|r4Gg6-f`Bli@(gOW)e1{=X zlx-R$$*Zhu3CJ5z|LJ;3exXj5uY^4EOWd-75(PlK>iD@fc5qO literal 0 HcmV?d00001 diff --git a/src/main/resources/com/android/tools/build/bundletool/internal/dex/classes.dex b/src/main/resources/com/android/tools/build/bundletool/internal/dex/classes.dex deleted file mode 100644 index fd41fe00e655d39767eec2170b478f8847952d83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6628 zcmcJUYit}>701ua?5^$gdc9sh*4{YDW^IS0NxXLah?7m5IQAxWvrZhxar2^Pc6V$~ zGM?GYjMJn^0>rIeS1K;lD0`=KiQ z&)r#P^M(&h^7Fa(+;h);oqO-jZb3T}?CBez!A9-M_IIBC(#{uu^l^88aJM%7&Vkon zz5Ldn+K7robY{9g3L%~*=pI9g^_}37OtcEJNg)bBp7#;;L%s)D#rXiyD1t@s5V!!I1}}r(0jZ8?E!YEc;3@DDcnf?4`octq z!AWotJP+OkS3p}m(H3BUZ-ZZhzk#|4>VXk(7}(%*;92lv@CJAXY-~V(!8kYubYKAo zoBrU|5v7JLe<1M9(EU<23)Hi2%?57OXna1TIO z5ki`>h*N}#NKYY+&=Be4NaH^t+{MH8K~R9QoHi=bu4m76_(52T`sY18=lS8f++Nw9 z$H@=td90T_dmb-8Y|H5DbRjz9)IeGpN~`XE+#T1X!)3hAfQLI&t8WGz;77P1bbJPa8| zjUr?OemL$}vY!S|Mm^c+$tF)WFH40+V9)s}A?xUVA*+P{X6&$|p8lASaViKIp_4*3 zk|AVip4g8i@=f%xkj-=fvc+q!1v@e;@=-D&S72A}6>=qMknQ+)3UVbzcoA}yS8p{e zCy`%`5g&pq?|I(C{NS1p&u6jWYp~&Yy_s_PzwtZ){WnnF2+8Yzefig+{~pR$L2^7? zU;cgQUq<BZmhvL2cLi{6hGIL3Z~cKIwwBJzp} znRZfK3X++q#_EJ9DwzqNNX6wUGJ67)@If2xNOqv7ylX?sxFSn&X)~Ql^kF}TP{S{3 z6eArd4@yH)6@2!hb`?c@W`tV}Ne!4BpXh@`Z=(+!3y7x38uTh8#pMj_n}okZD5|GA zsUFsR7GlPvP*{QFy>c0Kd5z6T0{M{e0Eu2SieA_eUbT?0DbMvEm**Nqm}{Tt3GO}U z`SZ$FS)q04DaReJk%E8R_CHb1F}FeH6S-8r&R>l1{UR(Si8w~tM>TrGwLV6j4ai#w z31>i(cY1`+`=k``<}s?oeHj|}$x0l7#Ix8%A-UL7OT)66h+q_E&w2_d>uTzxVq_(D zNk}#$HSo`U<34gblH^PBn(~}9Ay)aWF{$)_S4(BF4uSz7m@8QCPf`k~=|r%{*pq#3Cfb>q@b^msFR5QLu^=tHm! zVGnJ9RTyg(#%zRfmxqt(N1xT^CKTGI=*KaE;XkZWqyu%d1I6{=n38ky;hlHWsH|qf zyX15CZSSOJ$qjEms^H%K&XYUuS-~~@XZ;fHr106NrDjBGp>{jk4pWn~KfG6=dpkDi ziZa!{&9Cg(PGNbh5Pl5@I**(0E0%mu^H^3w!}&FUC7*39OLJJdx7m*GZ`N~6l=2-I zW9i|Uh3)h*j|$1;faat*vRnI*#3PpW1|$@b!aR*Ix}@-B14f~i@H%5 zvCLV0uKR>~S|$I8VL2N4M>D$)?ICH58plk>RSiS5m6%m^Lo1M!B{`cVC7T`NKb$*C zEm_qp*p^;Msl{Td+^saNxmIszwmYh;hBenITG#9}wcW}jO&gYr@|Njprn~HUpXKhh z7R^HDOkOMUOr)vf2IF0}r55t4<4$UMO+U?zt-ir%#9Am?CVcNx7qrUWl$*p1x!M^w zO}#hNEe&N1<@T(a*E;u`BZi(onbjS1mYcge{Ia*}s-&s)2CwYTM0=e#loS_I!^I++ z5DPM9&RS{OdNZpDHGfi_)5cX3f9*~l+J7};XyDqaX=+&ZZ)$GpkgcbwaamLS##F}4W8a~#BZlfYXwt8kXLeM+}dyRox*T}9qddCe@#&{S-z)(YjwylNlRPAzI?9_G=ClKr_8S{^Z5 zsXOQ`Kx`sAd}QX})c(oL%*61>-r+r&nSI0K8H(LFx21!qW>V8jCl4K*n%5FIQxaWH zLU$7RMcc;aFwQ1aJ3o&-T}WWj5(|rto5*PiyIc?YS=(AbVZxf7)$=%WgcrxPY>nD) zu|B2NrD+OHXC@EwnK`n5G;=j4dhMGY8^fVDjU#ZHkHBdhfznZuk7lX<*!45&Da4eP z=crcA=QYRKZK!h&t@mQCTNk^&n}f|VyT^|#Pt{}w^EZ+irTEQFr^hn)Q#Gb&23t>~ zNRD?~#e9$yr`suDhG z_56&XE}E!eQ%gl=CWj+)&gMJiT0Q!tdHRM*`;uvuK4-$9K-FHNcR9700;M&jCf%7Z z)UyXU_PiSvHEIl86TI;pBymw?)tqKf%Ytg3)ZA^UR6%ob;W|6?!rfgC1xjZg`NSgo z`L9EftDgWSHD}Rqsmdx{K~#-IX`yN?)4TsDW)(GaxhoU68+KbZrkDIh)yB;q62ja1 z6tsoU5vzc?cU0^((Ru9sgE+Y~s&jbmku`WxDa6_GWlB}ZjjFCnK_tcz?r4l6Nbl1Y zU0XGFiI^0n-jY!RN+)m-;ZQ*%n-(z0Ey=byy|R1h~!T?iwjP2 z8&}?#T+q!;YEe&a>mA&f#HR@d0g5`CyEk_a_9q{vPvi?1E}To=`nsvS2T`$i^H6SR z%WMwq-MUW2oro%1w)FJPZt2^6hbrFv$Ze=YTXO>gy?3bK-Ib^?IG8VN?H?-KzCs$7 z`JUN;LTzg@b_st#4p9KUi++HV$raUUN5w277WjwSNc;AjhOfrPe07Ro>xi z(_{O~iTQG3Np6EhyVBaw8f|Tq`YzT(PXUy)K6=sjjL20Z*M{5+_58XOY>OlP zIzIPHv;wL3q4g`ZZSAQ2_XbJ+s9u&|YVgS)M||=djh)h^s3biXm3;5wEmoH9g4GL6 zQtPLk{7)lgelGy8Og-`Qz8+`@MYm{izeq3ea^y8j$>5 zunSqX<9_qQZ cannot be parsed to a Long."); + } + private void createAppBundle(Path path) throws Exception { createAppBundle(path, /* codeTransparency= */ Optional.empty()); } @@ -1575,42 +1766,8 @@ private void createAppBundle(Path path, Optional codeTranspare new AppBundleSerializer().writeToDisk(appBundle.build(), path); } - private static void createKeyStore(Path keystorePath, String keystorePassword) - throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { - KeyStore keystore = KeyStore.getInstance("JKS"); - keystore.load(/* stream= */ null, keystorePassword.toCharArray()); - keystore.store(new FileOutputStream(keystorePath.toFile()), keystorePassword.toCharArray()); - } - - private static void addKeyToKeyStore( - Path keystorePath, - String keystorePassword, - String keyAlias, - String keyPassword, - PrivateKey privateKey, - X509Certificate certificate) - throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { - KeyStore keystore = KeyStore.getInstance("JKS"); - keystore.load(new FileInputStream(keystorePath.toFile()), keystorePassword.toCharArray()); - keystore.setKeyEntry( - keyAlias, privateKey, keyPassword.toCharArray(), new Certificate[] {certificate}); - keystore.store(new FileOutputStream(keystorePath.toFile()), keystorePassword.toCharArray()); - } - - private static SigningConfiguration createDebugKeystore(Path path) throws Exception { - KeyPair keyPair = KeyPairGenerator.getInstance("RSA").genKeyPair(); - PrivateKey privateKey = keyPair.getPrivate(); - X509Certificate certificate = - CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=Android Debug,O=Android,C=US"); - KeyStore keystore = KeyStore.getInstance("JKS"); - keystore.load(/* stream= */ null, DEBUG_KEYSTORE_PASSWORD.toCharArray()); - keystore.setKeyEntry( - DEBUG_KEY_ALIAS, - privateKey, - DEBUG_KEY_PASSWORD.toCharArray(), - new Certificate[] {certificate}); - keystore.store(new FileOutputStream(path.toFile()), DEBUG_KEYSTORE_PASSWORD.toCharArray()); - return SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build(); + private void createSdkBundle(Path path) throws Exception { + new SdkBundleSerializer().writeToDisk(new SdkBundleBuilder().build(), path); } private Path createKeystorePropertiesFile( @@ -1634,11 +1791,11 @@ private Path createKeystorePropertiesFile( /** Create an arbitrary but valid APK file. */ private Path createMinimalistSignedApkFile() { checkState( - standaloneApkSerializer != null, + apkSerializer != null, "The test must call TestComponent.useTestModule() to inject the required objects."); Path outPath = tmp.getRoot().toPath(); ZipPath zipPath = ZipPath.create("minimalist-apk.apk"); - standaloneApkSerializer.writeToDisk(createMinimalistModuleSplit(), outPath, zipPath); + apkSerializer.serialize(outPath, zipPath.toString(), createMinimalistModuleSplit()); return outPath.resolve(zipPath.toString()); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerOldSerializerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerOldSerializerTest.java new file mode 100644 index 00000000..8d423036 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerOldSerializerTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ +package com.android.tools.build.bundletool.commands; + + +import com.android.tools.build.bundletool.testing.TestModule; +import org.junit.experimental.theories.Theories; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class BuildApksManagerOldSerializerTest extends BuildApksManagerTest { + + @Override + protected TestModule.Builder createTestModuleBuilder() { + return TestModule.builder().withEnableApkSerializerWithoutBundleRecompression(false); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java index 3bfc0474..bb576b2f 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -29,6 +29,7 @@ import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.commands.BuildApksCommand.OutputFormat.DIRECTORY; +import static com.android.tools.build.bundletool.commands.ExtractApksCommand.ALL_MODULES_SHORTCUT; import static com.android.tools.build.bundletool.model.AndroidManifest.DEVELOPMENT_SDK_VERSION; import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; @@ -37,7 +38,7 @@ import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_SOURCE_METADATA_KEY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MDPI_VALUE; import static com.android.tools.build.bundletool.model.utils.ResultUtils.apexApkVariants; -import static com.android.tools.build.bundletool.model.utils.ResultUtils.hibernatedApkVariants; +import static com.android.tools.build.bundletool.model.utils.ResultUtils.archivedApkVariants; import static com.android.tools.build.bundletool.model.utils.ResultUtils.instantApkVariants; import static com.android.tools.build.bundletool.model.utils.ResultUtils.splitApkVariants; import static com.android.tools.build.bundletool.model.utils.ResultUtils.standaloneApkVariants; @@ -109,6 +110,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMultiAbiTargetingFromAllTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; +import static com.android.tools.build.bundletool.testing.TestUtils.extractAndroidManifest; import static com.android.tools.build.bundletool.testing.TestUtils.filesUnderPath; import static com.android.tools.build.bundletool.testing.truth.zip.TruthZip.assertThat; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -126,7 +128,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.aapt.ConfigurationOuterClass.Configuration; -import com.android.aapt.Resources.XmlNode; import com.android.apex.ApexManifestProto.ApexManifest; import com.android.apksig.ApkVerifier; import com.android.bundle.CodeTransparencyOuterClass.CodeRelatedFile; @@ -177,13 +178,13 @@ import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.SourceStamp; import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.exceptions.InvalidVersionCodeException; import com.android.tools.build.bundletool.model.utils.CertificateHelper; import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; -import com.android.tools.build.bundletool.testing.Aapt2Helper; import com.android.tools.build.bundletool.testing.ApkSetUtils; import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleConfigBuilder; @@ -207,13 +208,11 @@ import com.google.common.truth.Correspondence; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Int32Value; import dagger.Component; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; @@ -292,12 +291,16 @@ public class BuildApksManagerTest { @Inject BuildApksManager buildApksManager; @Inject BuildApksCommand command; + protected TestModule.Builder createTestModuleBuilder() { + return TestModule.builder(); + } + @BeforeClass public static void setUpClass() throws Exception { // Creating a new key takes in average 75ms (with peaks at 200ms), so creating a single one for // all the tests. KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(/* keySize= */ 3072); + kpg.initialize(/* keysize= */ 3072); KeyPair keyPair = kpg.genKeyPair(); privateKey = keyPair.getPrivate(); certificate = CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=BuildApksCommandTest"); @@ -305,7 +308,7 @@ public static void setUpClass() throws Exception { @Before public void setUp() throws Exception { - TestComponent.useTestModule(this, TestModule.builder().build()); + TestComponent.useTestModule(this, createTestModuleBuilder().build()); tmpDir = tmp.getRoot().toPath(); outputDir = tmp.newFolder("output").toPath(); @@ -333,7 +336,7 @@ public void tearDown() throws Exception { public void parallelExecutionSucceeds() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withExecutorService(MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(3))) .build()); @@ -373,7 +376,8 @@ public void selectsRightModules() throws Exception { withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -410,7 +414,7 @@ public void selectsRightModules_universalMode_withModulesFlag() throws Exception AppBundle appBundle = createAppBundleWithBaseAndFeatureModules("ar", "vr"); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -435,12 +439,41 @@ public void selectsRightModules_universalMode_withModulesFlag() throws Exception .build()); } + @Test + public void selectsRightModules_universalMode_withModulesFlag_allModulesShortcut() + throws Exception { + AppBundle appBundle = createAppBundleWithBaseAndFeatureModules("ar", "vr"); + TestComponent.useTestModule( + this, + createTestModuleBuilder() + .withAppBundle(appBundle) + .withOutputPath(outputFilePath) + .withApkBuildMode(UNIVERSAL) + .withModules(ALL_MODULES_SHORTCUT) + .build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(apkDescriptions(standaloneApkVariants(result))) + .ignoringRepeatedFieldOrder() + .containsExactly( + ApkDescription.newBuilder() + .setPath("universal.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setStandaloneApkMetadata( + StandaloneApkMetadata.newBuilder() + .addAllFusedModuleName(ImmutableList.of("base", "vr", "ar"))) + .build()); + } + @Test public void selectsRightModules_systemMode_withModulesFlag() throws Exception { AppBundle appBundle = createAppBundleWithBaseAndFeatureModules("ar", "vr"); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -497,7 +530,7 @@ public void systemMode_withModulesFlag_includesDependenciesOfModules() throws Ex .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -559,7 +592,7 @@ public void selectsRightModules_systemApks() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -645,7 +678,7 @@ public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits() thro .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -727,7 +760,7 @@ public void multipleModulesFusedAndNotFused_systemApks_hasCorrectAdditionalLangu .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -781,7 +814,8 @@ public void buildApksCommand_appTargetingAllSdks_buildsSplitAndStandaloneApks() builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -803,7 +837,8 @@ public void buildApksCommand_appTargetingLPlus_buildsSplitApksOnly() throws Exce androidManifest("com.test.app", withMinSdkVersion(ANDROID_L_API_VERSION)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); @@ -828,7 +863,8 @@ public void buildApksCommand_appTargetingPreL_buildsStandaloneApksOnly() throws .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -852,7 +888,8 @@ public void buildApksCommand_minSdkVersion_presentInStandaloneSdkTargeting() thr androidManifest("com.app", withMinSdkVersion(15), withMaxSdkVersion(20)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -877,7 +914,8 @@ public void buildApksCommand_maxSdkVersion_createsExtraAlternative() throws Exce androidManifest("com.app", withMinSdkVersion(21), withMaxSdkVersion(25)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -902,7 +940,7 @@ public void buildApksCommand_instantMode() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(INSTANT) @@ -930,7 +968,7 @@ public void buildApksCommand_persistentMode() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(PERSISTENT) @@ -962,7 +1000,7 @@ public void buildApksCommand_instantModeWithAssetModules() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(INSTANT) @@ -989,7 +1027,7 @@ public void buildApksCommand_persistentModeWithAssetModules() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(PERSISTENT) @@ -1037,7 +1075,7 @@ public void buildApksCommand_universal_selectsRightModulesForMerging() throws Ex .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1106,7 +1144,7 @@ public void buildApksCommand_universal_mergeApplicationElementsFromFeatureManife .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1125,7 +1163,7 @@ public void buildApksCommand_universal_mergeApplicationElementsFromFeatureManife assertThat(universalApk.getStandaloneApkMetadata().getFusedModuleNameList()) .containsExactly("base", "feature"); File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); - AndroidManifest manifest = extractAndroidManifest(universalApkFile); + AndroidManifest manifest = extractAndroidManifest(universalApkFile, tmpDir); Map refIdByActivity = transformValues( @@ -1184,7 +1222,7 @@ public void buildApksCommand_universal_mergeActivitiesFromFeatureManifest_0_13_4 .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1204,7 +1242,7 @@ public void buildApksCommand_universal_mergeActivitiesFromFeatureManifest_0_13_4 .containsExactly("base", "feature"); File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); - AndroidManifest manifest = extractAndroidManifest(universalApkFile); + AndroidManifest manifest = extractAndroidManifest(universalApkFile, tmpDir); Map refIdByActivity = transformValues( manifest.getActivitiesByName(), @@ -1253,7 +1291,7 @@ public void buildApksCommand_universal_generatesSingleApkWithNoOptimizations() t .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1314,7 +1352,7 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1377,7 +1415,7 @@ public void buildApksCommand_universal_generatesSingleApkWithSingleTcf() throws TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1436,7 +1474,7 @@ public void buildApksCommand_universal_generatesSingleApkWithSingleFallbackTcf() .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1498,7 +1536,7 @@ public void buildApksCommand_universal_generatesSingleApkWithSuffixStrippedTcfAs .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1560,7 +1598,7 @@ public void buildApksCommand_universal_strip64BitLibraries_doesNotStrip() throws .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1619,7 +1657,7 @@ public void buildApksCommand_universal_withAssetModules() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1690,7 +1728,7 @@ public void buildApksCommand_system_generatesSingleApkWithEmptyOptimizations() t .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -1746,7 +1784,7 @@ public void buildApksCommand_universalApk_variantUsesMinSdkFromManifest() throws .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(UNIVERSAL) @@ -1789,7 +1827,8 @@ public void buildApksCommand_splitApks_twoModulesOneOnDemand() throws Exception withFusingAttribute(true)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1828,7 +1867,8 @@ public void buildApksCommand_splitApks_targetLPlus() throws Exception { builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1858,7 +1898,8 @@ public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { .setManifest(androidManifest("com.test.app", withMinSdkVersion(25)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1889,7 +1930,8 @@ public void buildApksCommand_splitApks_honorsMaxSdkVersion() throws Exception { .setManifest(androidManifest("com.test.app", withMaxSdkVersion(21)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1913,7 +1955,8 @@ public void buildApksCommand_standalone_oneModuleOneVariant() throws Exception { builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1949,7 +1992,8 @@ public void disabledUncompressedNativeLibraries_singleSplitVariant() throws Exce BundleConfigBuilder.create().setUncompressNativeLibraries(false).build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -1981,7 +2025,8 @@ public void defaultUncompressedLibraries_after_0_6_0_enabled_multipleSplitVarian .setBundleConfig(BundleConfigBuilder.create().build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -2014,7 +2059,8 @@ public void defaultUncompressedLibraries_before_0_6_0_disabled_singleVariant() t .setBundleConfig(BundleConfigBuilder.create().setVersion("0.5.1").build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -2047,7 +2093,8 @@ public void enabledUncompressedNativeLibraries_nativeActivities_multipleSplitVar BundleConfigBuilder.create().setUncompressNativeLibraries(true).build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -2082,7 +2129,8 @@ public void enableNativeLibraryCompressionWithExternalStorage_multipleSplitVaria BundleConfigBuilder.create().setUncompressNativeLibraries(true).build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -2098,6 +2146,104 @@ public void enableNativeLibraryCompressionWithExternalStorage_multipleSplitVaria sdkVersionTargeting(P_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); } + @Test + public void enabledDexCompressionSplitter_enabledUncompressedDex_multipleSplitVariants() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) + .setBundleConfig(BundleConfigBuilder.create().setUncompressDexFiles(true).build()) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + ImmutableList splitApkVariants = splitApkVariants(result); + assertThat( + splitApkVariants.stream() + .map(variant -> variant.getTargeting().getSdkVersionTargeting())) + .containsExactly( + sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, Q_SDK_VERSION)), + sdkVersionTargeting(Q_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); + } + + @Test + public void enabledDexCompressionSplitter_disabledUncompressedDex_noUncompressedDexVariant() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) + .setBundleConfig(BundleConfigBuilder.create().setUncompressDexFiles(false).build()) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + ImmutableList splitApkVariants = splitApkVariants(result); + assertThat( + splitApkVariants.stream() + .map(variant -> variant.getTargeting().getSdkVersionTargeting())) + .containsExactly(sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION))); + } + + @Test + public void enableNativeLibsDexCompressionSplitterEnabled_multipleSplitVariants() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("lib/x86/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) + .setManifest(androidManifest("com.test.app"))) + .setBundleConfig( + BundleConfigBuilder.create() + .setUncompressNativeLibraries(true) + .setUncompressDexFiles(true) + .build()) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + ImmutableList splitApkVariants = splitApkVariants(result); + assertThat( + splitApkVariants.stream() + .map(variant -> variant.getTargeting().getSdkVersionTargeting())) + .containsExactly( + sdkVersionTargeting( + L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, M_SDK_VERSION, Q_SDK_VERSION)), + sdkVersionTargeting( + M_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, Q_SDK_VERSION)), + sdkVersionTargeting( + Q_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, M_SDK_VERSION))); + } @Test public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception { @@ -2122,7 +2268,7 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(ABI) @@ -2200,7 +2346,7 @@ public void buildApksCommand_system_withoutLanguageTargeting() throws Exception .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -2264,7 +2410,7 @@ public void buildApksCommand_system_uncompressedOptions() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -2376,7 +2522,7 @@ public void buildApksCommand_system_withLanguageTargeting() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(SYSTEM) @@ -2489,7 +2635,7 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(ABI) @@ -2567,7 +2713,7 @@ public void buildApksCommand_standalone_noTextureTargeting() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -2633,7 +2779,7 @@ public void buildApksCommand_standalone_mixedTextureTargeting() throws Exception .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -2704,7 +2850,7 @@ public void buildApksCommand_standalone_mixedTextureTargetingWithoutSuffixStripp .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -2771,7 +2917,7 @@ public void buildApksCommand_standalone_textureTargetingWithSuffixStripped() thr .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -2849,7 +2995,7 @@ public void buildApksCommand_standalone_mixedTextureTargetingWithSuffixStripped( .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -2923,7 +3069,7 @@ public void buildApksCommand_standalone_mixedTextureTargetingWithFallbackAsDefau .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) @@ -3008,13 +3154,14 @@ public void buildApksCommand_standalone_mixedTextureTargetingInDifferentPacksWit .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) .build()); - Exception e = assertThrows(IllegalStateException.class, () -> buildApksManager.execute()); + InvalidBundleException e = + assertThrows(InvalidBundleException.class, () -> buildApksManager.execute()); assertThat(e) .hasMessageThat() .contains("Encountered conflicting targeting values while merging assets config."); @@ -3077,13 +3224,14 @@ public void buildApksCommand_standalone_mixedTextureTargetingInDifferentPacksWit .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(TEXTURE_COMPRESSION_FORMAT) .build()); - Exception e = assertThrows(IllegalStateException.class, () -> buildApksManager.execute()); + InvalidBundleException e = + assertThrows(InvalidBundleException.class, () -> buildApksManager.execute()); assertThat(e) .hasMessageThat() .contains("Encountered conflicting targeting values while merging assets config."); @@ -3112,7 +3260,8 @@ public void buildApksCommand_standalone_mergesDexFiles() throws Exception { withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3172,7 +3321,8 @@ public void buildApksCommand_standalone_mergesDexFilesUsingMainDexList() throws "com.android.tools.build.bundletool", "mainDexList.txt", mainDexListFile) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3239,7 +3389,8 @@ public void buildApksCommand_standalone_deviceTierTargetingWithSuffixStripped() .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3284,7 +3435,7 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(ABI) @@ -3367,7 +3518,8 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle() throws Except .setManifest(androidManifest("com.test.app"))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3458,7 +3610,8 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle_previewTargetSd withMinSdkVersion("Q.fingerprint")))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3515,7 +3668,8 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle_hasRightSuffix( .setManifest(androidManifest("com.test.app"))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3581,7 +3735,8 @@ public void buildApksCommand_featureAndAssetModules_generatesAssetSlices( .addFile("assets/images/image2.jpg")) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3666,7 +3821,7 @@ public void buildApksCommand_assetOnly(@FromDataPoints("bundleVersion") Version .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withBundleConfig( BundleConfig.newBuilder() @@ -3794,7 +3949,7 @@ private void runSingleConcurrencyTest_disableNativeLibrariesOptimization(int thr TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withExecutorService( @@ -3905,7 +4060,8 @@ public void mergeInstallTimeModulesByDefault() throws Exception { .setBundleConfig(BundleConfigBuilder.create().setVersion("1.0.0").build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3955,7 +4111,8 @@ public void mergeInstallTimeDisabled() throws Exception { .setBundleConfig(BundleConfigBuilder.create().setVersion("1.0.0").build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -3993,7 +4150,7 @@ public void splitFileNames_abi() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withOptimizationDimensions(ABI) @@ -4044,7 +4201,8 @@ public void splitNames_assetLanguages() throws Exception { assetsDirectoryTargeting(alternativeLanguageTargeting("es")))))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4067,7 +4225,10 @@ public void splits_assetTextureCompressionFormatWithSuffixStripped() throws Exce TestComponent.useTestModule( this, - TestModule.builder().withBundlePath(bundlePath).withOutputPath(outputFilePath).build()); + createTestModuleBuilder() + .withBundlePath(bundlePath) + .withOutputPath(outputFilePath) + .build()); buildApksManager.execute(); @@ -4136,7 +4297,8 @@ public void splits_assetMixedTextureCompressionFormat() throws Exception { .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4219,7 +4381,8 @@ public void splits_assetMixedTextureTargetingWithSuffixStripped_featureModule() .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4299,7 +4462,8 @@ public void splits_assetMixedTextureTargetingWithSuffixStripped_assetModule() th .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4348,7 +4512,10 @@ public void splits_assetTextureCompressionFormatWithoutSuffixStripped() throws E TestComponent.useTestModule( this, - TestModule.builder().withBundlePath(bundlePath).withOutputPath(outputFilePath).build()); + createTestModuleBuilder() + .withBundlePath(bundlePath) + .withOutputPath(outputFilePath) + .build()); buildApksManager.execute(); @@ -4394,7 +4561,10 @@ public void splits_assetTextureCompressionFormatDisabled() throws Exception { TestComponent.useTestModule( this, - TestModule.builder().withBundlePath(bundlePath).withOutputPath(outputFilePath).build()); + createTestModuleBuilder() + .withBundlePath(bundlePath) + .withOutputPath(outputFilePath) + .build()); buildApksManager.execute(); @@ -4493,7 +4663,8 @@ public void deviceTieredAssets_inBaseModule() throws Exception { .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4576,7 +4747,8 @@ public void deviceTieredAssets_inAssetModule() throws Exception { .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4649,7 +4821,7 @@ public void deviceTieredAssets_withDeviceSpec_deviceTierSet() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withDeviceSpec( @@ -4722,7 +4894,7 @@ public void deviceTieredAssets_withDeviceSpec_deviceTierNotSet_defaultIsUsed() t .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withDeviceSpec( @@ -4777,7 +4949,8 @@ public void deviceGroupTargetedConditionalModule() throws Exception { .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -4799,7 +4972,7 @@ public void deviceGroupTargetedConditionalModule() throws Exception { public void apksSigned() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withSigningConfig( SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build()) @@ -4871,7 +5044,7 @@ public void extractApkSet_outputApksWithoutArchive() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputDir) .withOptimizationDimensions(ABI, LANGUAGE) @@ -4892,7 +5065,7 @@ public void extractApkSet_outputApksWithoutArchive() throws Exception { public void bundleToolVersionSet() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withOptimizationDimensions(ABI, LANGUAGE) .build()); @@ -4912,7 +5085,7 @@ public void overwriteSet() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withCustomBuildApksCommandSetter(command -> command.setOverwriteOutput(true)) .build()); @@ -4927,7 +5100,7 @@ public void externalExecutorServiceDoesNotShutDown() throws Exception { ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); TestComponent.useTestModule( - this, TestModule.builder().withExecutorService(listeningExecutorService).build()); + this, createTestModuleBuilder().withExecutorService(listeningExecutorService).build()); buildApksManager.execute(); assertThat(listeningExecutorService.isShutdown()).isFalse(); @@ -4937,7 +5110,7 @@ public void externalExecutorServiceDoesNotShutDown() throws Exception { public void apkModifier_modifyingVersionCode() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withApkModifier( new ApkModifier() { @@ -4995,7 +5168,8 @@ public void buildApksCommand_populatesDependencies() throws Exception { withFusingAttribute(true)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5037,7 +5211,10 @@ public void firstVariantNumber() throws Exception { TestComponent.useTestModule( this, - TestModule.builder().withFirstVariantNumber(100).withOutputPath(outputFilePath).build()); + createTestModuleBuilder() + .withFirstVariantNumber(100) + .withOutputPath(outputFilePath) + .build()); buildApksManager.execute(); @@ -5082,7 +5259,8 @@ public void buildApksCommand_instantApks_targetLPlus() throws Exception { .setManifest(androidManifest("com.test.app", withInstant(true)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5108,7 +5286,8 @@ public void buildApksCommand_instantApksAndSplitsGenerated() throws Exception { .setManifest(androidManifest("com.test.app", withInstant(true)))) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5164,7 +5343,8 @@ public void pinningOfManifestReachableResources_enabledSince_0_8_1() throws Exce .setBundleConfig(BundleConfigBuilder.create().setVersion("0.8.1").build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5230,7 +5410,8 @@ public void pinningOfManifestReachableResources_disabled( BundleConfigBuilder.create().setVersion(bundleVersion.toString()).build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5290,7 +5471,8 @@ public void explicitMdpiPreferredOverDefault_enabledSince_0_9_1( .build()) .build(); TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); @@ -5341,7 +5523,7 @@ public void allApksSignedWithV1_minSdkLessThan24() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withSigningConfig( @@ -5376,7 +5558,7 @@ public void allApksSignedWithV1_minSdkAtLeast24_oldBundletool() throws Exception TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withSigningConfig( @@ -5411,7 +5593,7 @@ public void allApksNotSignedWithV1_minSdkAtLeast24_recentBundletool() throws Exc TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withSigningConfig( @@ -5439,7 +5621,7 @@ public void apkWithSourceStamp() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withSigningConfig(signingConfiguration) .withSourceStamp( @@ -5462,7 +5644,7 @@ public void apkWithSourceStamp() throws Exception { assertThat(verifierResult.isSourceStampVerified()).isTrue(); assertThat(verifierResult.getSourceStampInfo().getCertificate()).isEqualTo(certificate); - AndroidManifest manifest = extractAndroidManifest(apk); + AndroidManifest manifest = extractAndroidManifest(apk, tmpDir); assertThat(manifest.getMetadataValue(STAMP_SOURCE_METADATA_KEY)).hasValue(stampSource); try (ZipFile apkZip = new ZipFile(apk)) { @@ -5483,7 +5665,7 @@ public void apkWithSourceStamp() throws Exception { public void packageNameIsPropagatedToBuildResult() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withOutputPath(outputFilePath) .withAppBundle( new AppBundleBuilder() @@ -5578,8 +5760,7 @@ public void transparencyFilePropagatedAsExpected() throws Exception { TestComponent.useTestModule( this, - TestModule.builder() - .withCustomBuildApksCommandSetter(command -> command.setEnableNewApkSerializer(true)) + createTestModuleBuilder() .withOutputPath(outputFilePath) .withAppBundle(appBundle.build()) .build()); @@ -5628,7 +5809,7 @@ public void transparencyFilePropagatedAsExpected() throws Exception { } @Test - public void buildApksCommand_hibernation_success() throws Exception { + public void buildApksCommand_archive_success() throws Exception { AppBundle appBundle = new AppBundleBuilder() .addModule("base", builder -> builder.setManifest(androidManifest("com.test.app"))) @@ -5636,7 +5817,7 @@ public void buildApksCommand_hibernation_success() throws Exception { .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(ARCHIVE) @@ -5651,25 +5832,25 @@ public void buildApksCommand_hibernation_success() throws Exception { assertThat(splitApkVariants(result)).isEmpty(); assertThat(standaloneApkVariants(result)).isEmpty(); assertThat(systemApkVariants(result)).isEmpty(); - assertThat(hibernatedApkVariants(result)).hasSize(1); - Variant hibernatedVariant = hibernatedApkVariants(result).get(0); - assertThat(hibernatedVariant.getTargeting()).isEqualToDefaultInstance(); + assertThat(archivedApkVariants(result)).hasSize(1); + Variant archivedVariant = archivedApkVariants(result).get(0); + assertThat(archivedVariant.getTargeting()).isEqualToDefaultInstance(); - assertThat(apkDescriptions(hibernatedVariant)).hasSize(1); - assertThat(hibernatedVariant.getApkSetList()).hasSize(1); - ApkSet apkSet = hibernatedVariant.getApkSet(0); + assertThat(apkDescriptions(archivedVariant)).hasSize(1); + assertThat(archivedVariant.getApkSetList()).hasSize(1); + ApkSet apkSet = archivedVariant.getApkSet(0); assertThat( apkSet.getApkDescriptionList().stream() .map(ApkDescription::getPath) .collect(toImmutableSet())) - .containsExactly("hibernation/hibernation.apk"); + .containsExactly("archive/archive.apk"); apkSet .getApkDescriptionList() .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); } @Test - public void buildApksCommand_hibernation_apex_hibernatedNotGenerated() throws Exception { + public void buildApksCommand_archive_apex_archivedNotGenerated() throws Exception { ApexImages apexConfig = apexImages(targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64"))); AppBundle appBundle = @@ -5682,7 +5863,7 @@ public void buildApksCommand_hibernation_apex_hibernatedNotGenerated() throws Ex .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withOutputPath(outputFilePath) .withApkBuildMode(ARCHIVE) @@ -5693,7 +5874,7 @@ public void buildApksCommand_hibernation_apex_hibernatedNotGenerated() throws Ex } @Test - public void buildApksCommand_hibernation_assetOnly_hibernatedNotGenerated() throws Exception { + public void buildApksCommand_archive_assetOnly_archivedNotGenerated() throws Exception { AppBundle appBundle = new AppBundleBuilder() .addModule("base", builder -> builder.setManifest(androidManifest("com.test.app"))) @@ -5701,7 +5882,7 @@ public void buildApksCommand_hibernation_assetOnly_hibernatedNotGenerated() thro .build(); TestComponent.useTestModule( this, - TestModule.builder() + createTestModuleBuilder() .withAppBundle(appBundle) .withBundleConfig(BundleConfig.newBuilder().setType(BundleType.ASSET_ONLY)) .withOutputPath(outputFilePath) @@ -5713,7 +5894,7 @@ public void buildApksCommand_hibernation_assetOnly_hibernatedNotGenerated() thro ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); - assertThat(hibernatedApkVariants(result)).isEmpty(); + assertThat(archivedApkVariants(result)).isEmpty(); } private static ImmutableList apkDescriptions(List variants) { @@ -5792,28 +5973,11 @@ private Path createAndStoreBundle(AppBundle appBundle) throws IOException { } private int extractVersionCode(File apk) { - return extractAndroidManifest(apk) + return extractAndroidManifest(apk, tmpDir) .getVersionCode() .orElseThrow(InvalidVersionCodeException::createMissingVersionCodeException); } - private AndroidManifest extractAndroidManifest(File apk) { - Path protoApkPath = tmpDir.resolve("proto.apk"); - Aapt2Helper.convertBinaryApkToProtoApk(apk.toPath(), protoApkPath); - try { - try (ZipFile protoApk = new ZipFile(protoApkPath.toFile())) { - return AndroidManifest.create( - XmlNode.parseFrom( - protoApk.getInputStream(protoApk.getEntry("AndroidManifest.xml")), - ExtensionRegistry.getEmptyRegistry())); - } finally { - Files.deleteIfExists(protoApkPath); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - private static ImmutableList getServicesFromManifest(AndroidManifest manifest) { return manifest .getManifestRoot() diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java index 62c257f5..57a5c452 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java @@ -212,7 +212,8 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { "res/drawable/image1.jpg", "res/drawable-fr/image1.jpg", "res/drawable/image2.jpg", - "res/xml/splits0.xml"); + "res/xml/splits0.xml", + "res/xml/locales_config.xml"); } ApkDescription baseFr = apkBaseMaster.get(/* isMasterSplit= */ false); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java new file mode 100644 index 00000000..2d3c8402 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; +import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_SERIAL; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkLibraryElement; +import static com.android.tools.build.bundletool.testing.TestUtils.addKeyToKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createDebugKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; +import static com.google.common.base.StandardSystemProperty.USER_HOME; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.stream; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Files.Assets; +import com.android.bundle.Files.NativeLibraries; +import com.android.tools.build.bundletool.commands.BuildSdkApksCommand.OutputFormat; +import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.flags.FlagParser.FlagParseException; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.flags.ParsedFlags.UnknownFlagsException; +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.files.FileUtils; +import com.android.tools.build.bundletool.testing.BundleConfigBuilder; +import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; +import com.android.tools.build.bundletool.testing.ResourceTableBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.concurrent.Executors; +import java.util.stream.Stream; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BuildSdkApksCommandTest { + + private static final String KEYSTORE_PASSWORD = "keystore-password"; + private static final String KEY_PASSWORD = "key-password"; + private static final String KEY_ALIAS = "key-alias"; + private static final String DEBUG_KEYSTORE_PASSWORD = "android"; + private static final String DEBUG_KEY_PASSWORD = "android"; + private static final String DEBUG_KEY_ALIAS = "AndroidDebugKey"; + private static final String DEVICE_ID = "id1"; + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final String PACKAGE_NAME = "com.test.sdk.detail"; + private static final BundleConfig BUNDLE_CONFIG = BundleConfigBuilder.create().build(); + private static final NativeLibraries NATIVE_CONFIG = NativeLibraries.getDefaultInstance(); + private static final ResourceTable RESOURCE_TABLE = + new ResourceTableBuilder().addPackage("com.app").build(); + private static final Assets ASSETS_CONFIG = Assets.getDefaultInstance(); + + private static final XmlNode MANIFEST = createSdkAndroidManifest(); + + private static PrivateKey privateKey; + private static X509Certificate certificate; + + private Path tmpDir; + private Path sdkBundlePath; + private Path outputFilePath; + private Path keystorePath; + + @BeforeClass + public static void setUpClass() throws Exception { + // Creating a new key takes in average 75ms (with peaks at 200ms), so creating a single one for + // all the tests. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(/* keysize= */ 3072); + KeyPair keyPair = kpg.genKeyPair(); + privateKey = keyPair.getPrivate(); + certificate = + CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=BuildSdkApksCommandTest"); + } + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + sdkBundlePath = tmpDir.resolve("SdkBundle.asb"); + outputFilePath = tmpDir.resolve("output.apks"); + + // Keystore. + keystorePath = tmpDir.resolve("keystore.jks"); + createKeystore(keystorePath, KEYSTORE_PASSWORD); + addKeyToKeystore( + keystorePath, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD, privateKey, certificate); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult() { + BuildSdkApksCommand commandViaFlags = + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--version-code=351", + "--output-format=DIRECTORY", + "--aapt2=path/to/aapt2", + "--ks=" + keystorePath, + "--ks-key-alias=" + KEY_ALIAS, + "--ks-pass=pass:" + KEYSTORE_PASSWORD, + "--key-pass=pass:" + KEY_PASSWORD, + "--verbose")); + + BuildSdkApksCommand.Builder commandViaBuilder = + BuildSdkApksCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setOutputFile(outputFilePath) + .setVersionCode(351) + .setOutputFormat(OutputFormat.DIRECTORY) + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setSigningConfiguration( + SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build()) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setVerbose(true); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void buildingCommandViaFlags_sdkBundlePathNotSet() { + Throwable e = + assertThrows( + RequiredFlagNotSetException.class, + () -> BuildSdkApksCommand.fromFlags(new FlagParser().parse(""))); + assertThat(e).hasMessageThat().contains("Missing the required --sdk-bundle flag"); + } + + @Test + public void outputNotSetViaFlags_throws() { + expectMissingRequiredFlagException( + "output", + () -> + BuildSdkApksCommand.fromFlags(new FlagParser().parse("--sdk-bundle=" + sdkBundlePath))); + } + + @Test + public void outputNotSetViaBuilder_throws() { + expectMissingRequiredBuilderPropertyException( + "outputFile", () -> BuildSdkApksCommand.builder().setSdkBundlePath(sdkBundlePath).build()); + } + + @Test + public void bundleNotSetViaFlags_throws() { + expectMissingRequiredFlagException( + "sdk-bundle", + () -> BuildSdkApksCommand.fromFlags(new FlagParser().parse("--output=" + outputFilePath))); + } + + @Test + public void keystoreSet_keyAliasNotSet_throws() { + InvalidCommandException e = + assertThrows( + InvalidCommandException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--ks=" + keystorePath))); + assertThat(e).hasMessageThat().isEqualTo("Flag --ks-key-alias is required when --ks is set."); + } + + @Test + public void keyAliasSet_keystoreNotSet_throws() { + InvalidCommandException e = + assertThrows( + InvalidCommandException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--ks-key-alias=" + KEY_ALIAS))); + assertThat(e).hasMessageThat().isEqualTo("Flag --ks is required when --ks-key-alias is set."); + } + + @Test + public void bundleNotSetViaBuilder_throws() { + expectMissingRequiredBuilderPropertyException( + "sdkBundlePath", () -> BuildSdkApksCommand.builder().setOutputFile(outputFilePath).build()); + } + + @Test + public void overwriteSetForDirectoryOutputFormat_throws() throws Exception { + createBasicZipBuilderWithManifest().writeTo(sdkBundlePath); + ParsedFlags flags = + getDefaultFlagsWithAdditionalFlags("--overwrite", "--output-format=directory"); + BuildSdkApksCommand command = BuildSdkApksCommand.fromFlags(flags); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e).hasMessageThat().contains("flag is not supported"); + } + + @Test + public void overwriteNotSetOutputFileAlreadyExists_throws() throws Exception { + createBasicZipBuilderWithManifest().writeTo(sdkBundlePath); + new ZipBuilder() + .addFileWithContent(ZipPath.create("BundleConfig.pb"), BUNDLE_CONFIG.toByteArray()) + .writeTo(outputFilePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e).hasMessageThat().contains("already exists"); + } + + @Test + public void nonPositiveMaxThreads_throws() throws Exception { + FlagParseException zeroException = + assertThrows( + FlagParseException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--max-threads=0"))); + assertThat(zeroException).hasMessageThat().contains("flag --max-threads has illegal value"); + + FlagParseException negativeException = + assertThrows( + FlagParseException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--max-threads=-3"))); + assertThat(negativeException).hasMessageThat().contains("flag --max-threads has illegal value"); + } + + @Test + public void unknownFlag_throws() { + UnknownFlagsException exception = + assertThrows( + UnknownFlagsException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--unknownFlag=notSure"))); + + assertThat(exception) + .hasMessageThat() + .contains(String.format("Unrecognized flags: --%s", "unknownFlag")); + } + + @Test + public void missingBundleFile_throws() { + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e).hasMessageThat().contains("not found"); + } + + @Test + public void bundleMissingFiles_throws() throws Exception { + ZipBuilder zipBuilder = new ZipBuilder(); + zipBuilder.writeTo(sdkBundlePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e).hasMessageThat().contains("missing required file"); + } + + // Ensures that validations are run on the bundle zip file. + @Test + public void bundleMultipleModules_throws() throws Exception { + createBasicZipBuilderWithManifest() + .addFileWithProtoContent(ZipPath.create("feature/manifest/AndroidManifest.xml"), MANIFEST) + .addFileWithContent(ZipPath.create("feature/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("feature/assets.pb"), ASSETS_CONFIG.toByteArray()) + .addFileWithContent(ZipPath.create("feature/native.pb"), NATIVE_CONFIG.toByteArray()) + .addFileWithContent(ZipPath.create("feature/resources.pb"), RESOURCE_TABLE.toByteArray()) + .addFileWithContent(ZipPath.create("feature/lib/x86/libfoo.so"), DUMMY_CONTENT) + .writeTo(sdkBundlePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e).hasMessageThat().contains("SDK bundles need exactly one module"); + } + + // Ensures that validations are run on the bundle object. + @Test + public void invalidManifest_throws() throws Exception { + createZipBuilderWithInvalidManifest().writeTo(sdkBundlePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains( + "SDK Major version in <" + SDK_LIBRARY_ELEMENT_NAME + "> cannot be parsed to a Long."); + } + + @Test + public void executeCreatesFile() throws Exception { + createBasicZipBuilderWithManifest().writeTo(sdkBundlePath); + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()).execute(); + assertThat(Files.exists(outputFilePath)).isTrue(); + } + + @Test + public void internalExecutorIsShutDownAfterExecute() throws Exception { + createBasicZipBuilderWithManifest().writeTo(sdkBundlePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags("--max-threads=16")); + command.execute(); + + assertThat(command.getExecutorService().isShutdown()).isTrue(); + } + + @Test + public void externalExecutorIsNotShutDownAfterExecute() throws Exception { + createBasicZipBuilderWithManifest().writeTo(sdkBundlePath); + BuildSdkApksCommand command = + BuildSdkApksCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setOutputFile(outputFilePath) + .setExecutorService( + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(/* nThreads= */ 4))) + .build(); + command.execute(); + + assertThat(command.getExecutorService().isShutdown()).isFalse(); + } + + @Test + public void printHelpDoesNotCrash() { + BuildSdkApksCommand.help(); + } + + @Test + @Ignore("b/222682673") + public void noKeystoreProvidedPrintsWarning() throws Exception { + try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); + PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags(), + outputPrintStream, + new DefaultSystemEnvironmentProvider()); + + assertThat(new String(outputByteArrayStream.toByteArray(), UTF_8)) + .contains("WARNING: The APKs won't be signed"); + } + } + + @Test + public void noKeystoreProvidedPrintsInfo_debugKeystore() throws Exception { + SystemEnvironmentProvider provider = + new FakeSystemEnvironmentProvider( + /* variables= */ ImmutableMap.of( + ANDROID_HOME, "/android/home", ANDROID_SERIAL, DEVICE_ID), + /* properties= */ ImmutableMap.of(USER_HOME.key(), tmpDir.toString())); + Path debugKeystorePath = tmpDir.resolve(".android").resolve("debug.keystore"); + FileUtils.createParentDirectories(debugKeystorePath); + createDebugKeystore( + debugKeystorePath, DEBUG_KEYSTORE_PASSWORD, DEBUG_KEY_ALIAS, DEBUG_KEY_PASSWORD); + + try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); + PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags(), outputPrintStream, provider); + + assertThat(new String(outputByteArrayStream.toByteArray(), UTF_8)) + .contains("INFO: The APKs will be signed with the debug keystore"); + } + } + + @Test + public void keystoreProvidedDoesNotPrint() throws Exception { + try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); + PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--ks=" + keystorePath, + "--ks-key-alias=" + KEY_ALIAS, + "--ks-pass=pass:" + KEYSTORE_PASSWORD, + "--key-pass=pass:" + KEY_PASSWORD), + outputPrintStream, + new DefaultSystemEnvironmentProvider()); + + assertThat(new String(outputByteArrayStream.toByteArray(), UTF_8)).isEmpty(); + } + } + + @Test + public void verboseIsFalseByDefault() { + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + assertThat(command.getVerbose()).isFalse(); + } + + private ZipBuilder createBasicZipBuilderWithManifest() { + ZipBuilder zipBuilder = new ZipBuilder(); + zipBuilder + .addFileWithContent(ZipPath.create("BundleConfig.pb"), BUNDLE_CONFIG.toByteArray()) + .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/assets.pb"), ASSETS_CONFIG.toByteArray()) + .addFileWithContent(ZipPath.create("base/native.pb"), NATIVE_CONFIG.toByteArray()) + .addFileWithContent(ZipPath.create("base/resources.pb"), RESOURCE_TABLE.toByteArray()) + .addFileWithContent(ZipPath.create("base/lib/x86/libfoo.so"), DUMMY_CONTENT) + .addFileWithContent( + ZipPath.create("BUNDLE-METADATA/some.namespace/metadata1"), new byte[] {0x01}); + return zipBuilder; + } + + private static XmlNode createSdkAndroidManifest() { + return androidManifest( + PACKAGE_NAME, + withMinSdkVersion(32), + withSdkLibraryElement("15"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "5")); + } + + private ZipBuilder createZipBuilderWithInvalidManifest() { + XmlNode invalidManifest = + androidManifest( + PACKAGE_NAME, + withMinSdkVersion(32), + withSdkLibraryElement("NotALong"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "5")); + return new ZipBuilder() + .addFileWithContent(ZipPath.create("BundleConfig.pb"), BUNDLE_CONFIG.toByteArray()) + .addFileWithProtoContent( + ZipPath.create("base/manifest/AndroidManifest.xml"), invalidManifest) + .addFileWithContent( + ZipPath.create("BUNDLE-METADATA/some.namespace/metadata1"), new byte[] {0x01}); + } + + private ParsedFlags getDefaultFlagsWithAdditionalFlags(String... additionalFlags) { + String[] flags = + Stream.concat(getDefaultFlagList().stream(), stream(additionalFlags)) + .toArray(String[]::new); + return new FlagParser().parse(flags); + } + + private ImmutableList getDefaultFlagList() { + return ImmutableList.of("--sdk-bundle=" + sdkBundlePath, "--output=" + outputFilePath); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java new file mode 100644 index 00000000..d398ba7a --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_SANDBOX_MIN_VERSION; +import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_CODE_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.ModuleSplit.DEFAULT_SDK_PATCH_VERSION; +import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractFromApkSetFile; +import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractTocFromSdkApkSetFile; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkLibraryElement; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.DEFAULT_BUNDLE_CONFIG; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.PACKAGE_NAME; +import static com.android.tools.build.bundletool.testing.TestUtils.addKeyToKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.extractAndroidManifest; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import com.android.apksig.ApkVerifier; +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.BuildSdkApksResult; +import com.android.bundle.Commands.SdkVersionInformation; +import com.android.bundle.Commands.Variant; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.io.SdkBundleSerializer; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.io.ZipReader; +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.android.tools.build.bundletool.testing.SdkBundleBuilder; +import java.io.File; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.theories.Theories; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class BuildSdkApksManagerTest { + + private static final String KEYSTORE_PASSWORD = "keystore-password"; + private static final String KEY_PASSWORD = "key-password"; + private static final String KEY_ALIAS = "key-alias"; + private static PrivateKey privateKey; + private static X509Certificate certificate; + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + private Path tmpDir; + private Path sdkBundlePath; + private Path outputFilePath; + private Path keystorePath; + + @BeforeClass + public static void setUpClass() throws Exception { + // Creating a new key takes in average 75ms (with peaks at 200ms), so creating a single one for + // all the tests. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(/* keysize= */ 3072); + KeyPair keyPair = kpg.genKeyPair(); + privateKey = keyPair.getPrivate(); + certificate = + CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=BuildSdkApksCommandTest"); + } + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + sdkBundlePath = tmpDir.resolve("SdkBundle.asb"); + outputFilePath = tmpDir.resolve("output.apks"); + + // Keystore. + keystorePath = tmpDir.resolve("keystore.jks"); + createKeystore(keystorePath, KEYSTORE_PASSWORD); + addKeyToKeystore( + keystorePath, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD, privateKey, certificate); + } + + @Test + public void tocIsCorrect() throws Exception { + execute(new SdkBundleBuilder().build()); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + assertThat(result.getVariantCount()).isEqualTo(1); + assertThat(result.getPackageName()).isEqualTo(PACKAGE_NAME); + assertThat(result.getBundletool().getVersion()) + .isEqualTo(DEFAULT_BUNDLE_CONFIG.getBundletool().getVersion()); + } + + @Test + public void manifestIsMutated() throws Exception { + Integer versionCode = 1253; + execute(new SdkBundleBuilder().setVersionCode(versionCode).build()); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + Variant variant = result.getVariant(0); + + ApkDescription apkDescription = variant.getApkSet(0).getApkDescription(0); + + File apkFile = extractFromApkSetFile(apkSetFile, apkDescription.getPath(), tmpDir); + AndroidManifest manifest = extractAndroidManifest(apkFile, tmpDir); + + assertThat(manifest.getMinSdkVersion()).hasValue(SDK_SANDBOX_MIN_VERSION); + assertThat( + manifest + .getManifestRoot() + .getElement() + .getAndroidAttribute(VERSION_NAME_RESOURCE_ID) + .get() + .getValueAsString()) + .isEqualTo("15.0.5"); + assertThat( + manifest + .getManifestRoot() + .getElement() + .getAndroidAttribute(VERSION_CODE_RESOURCE_ID) + .get() + .getValueAsDecimalInteger()) + .isEqualTo(versionCode); + } + + @Test + public void sdkManifestMutation_highMinSdkVersion_minSdkVersionUnchanged() throws Exception { + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest( + PACKAGE_NAME, + withMinSdkVersion(SDK_SANDBOX_MIN_VERSION + 5), + withSdkLibraryElement("20"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "12"))) + .build()) + .build(); + + execute(sdkBundle); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + Variant variant = result.getVariant(0); + + ApkDescription apkDescription = variant.getApkSet(0).getApkDescription(0); + + File apkFile = extractFromApkSetFile(apkSetFile, apkDescription.getPath(), tmpDir); + AndroidManifest manifest = extractAndroidManifest(apkFile, tmpDir); + + assertThat(manifest.getMinSdkVersion()).hasValue(SDK_SANDBOX_MIN_VERSION + 5); + } + + @Test + public void sdkManifestMutation_patchVersionNotSet_defaultPatchVersionAdded() throws Exception { + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest(androidManifest(PACKAGE_NAME, withSdkLibraryElement("20"))) + .build()) + .build(); + + execute(sdkBundle); + + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + Variant variant = result.getVariant(0); + ApkDescription apkDescription = variant.getApkSet(0).getApkDescription(0); + File apkFile = extractFromApkSetFile(apkSetFile, apkDescription.getPath(), tmpDir); + AndroidManifest manifest = extractAndroidManifest(apkFile, tmpDir); + + assertThat(manifest.getMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME)) + .hasValue(DEFAULT_SDK_PATCH_VERSION); + } + + @Test + public void apksSigned() throws Exception { + execute(new SdkBundleBuilder().build()); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + Variant variant = result.getVariant(0); + ApkDescription apkDescription = variant.getApkSet(0).getApkDescription(0); + File apkFile = extractFromApkSetFile(apkSetFile, apkDescription.getPath(), tmpDir); + + ApkVerifier.Result verifierResult = new ApkVerifier.Builder(apkFile).build().verify(); + assertThat(verifierResult.isVerified()).isTrue(); + assertThat(verifierResult.getSignerCertificates()).containsExactly(certificate); + } + + @Test + public void sdkVersionInformationIsSet() throws Exception { + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest( + PACKAGE_NAME, + withSdkLibraryElement("100"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "132"))) + .build()) + .setVersionCode(99) + .build(); + + execute(sdkBundle); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + SdkVersionInformation version = result.getVersion(); + + assertThat(version.getVersionCode()).isEqualTo(99); + assertThat(version.getMajor()).isEqualTo(100); + assertThat(version.getPatch()).isEqualTo(132); + } + + @Test + public void sdkPatchVersionIsSetToDefaultValue() throws Exception { + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest(androidManifest(PACKAGE_NAME, withSdkLibraryElement("1181894"))) + .build()) + .build(); + + execute(sdkBundle); + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + BuildSdkApksResult result = extractTocFromSdkApkSetFile(apkSetFile, tmpDir); + + SdkVersionInformation version = result.getVersion(); + + assertThat(version.getPatch()).isEqualTo(Long.parseLong(DEFAULT_SDK_PATCH_VERSION)); + } + + private BuildSdkApksCommand createCommand() { + return BuildSdkApksCommand.fromFlags( + new FlagParser() + .parse( + "--sdk-bundle=" + sdkBundlePath, + "--output=" + outputFilePath, + "--ks=" + keystorePath, + "--ks-key-alias=" + KEY_ALIAS, + "--ks-pass=pass:" + KEYSTORE_PASSWORD, + "--key-pass=pass:" + KEY_PASSWORD)); + } + + private void execute(SdkBundle sdkBundle) throws Exception { + new SdkBundleSerializer().writeToDisk(sdkBundle, sdkBundlePath); + + try (ZipReader zipReader = ZipReader.createFromFile(sdkBundlePath)) { + DaggerBuildSdkApksManagerComponent.builder() + .setBuildSdkApksCommand(createCommand()) + .setTempDirectory(new TempDirectory(getClass().getSimpleName())) + .setSdkBundle(sdkBundle) + .setZipReader(zipReader) + .setUseBundleCompression(false) + .build() + .create() + .execute(); + } + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java index 6fe92612..083f4b61 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java @@ -34,7 +34,7 @@ import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.flags.ParsedFlags.UnknownFlagsException; -import com.android.tools.build.bundletool.io.ApkSerializerHelper; +import com.android.tools.build.bundletool.io.ApkSerializer; import com.android.tools.build.bundletool.io.AppBundleSerializer; import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.model.AndroidManifest; @@ -90,7 +90,7 @@ public final class CheckTransparencyCommandTest { @Rule public final TemporaryFolder tmp = new TemporaryFolder(); - @Inject ApkSerializerHelper apkSerializerHelper; + @Inject ApkSerializer apkSerializer; private Path tmpDir; private Path bundlePath; @@ -111,7 +111,7 @@ public final class CheckTransparencyCommandTest { public void setUp() throws Exception { tmpDir = tmp.getRoot().toPath(); kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(/* keySize= */ 3072); + kpg.initialize(/* keysize= */ 3072); KeyPair keyPair = kpg.genKeyPair(); transparencyPrivateKey = keyPair.getPrivate(); @@ -137,11 +137,7 @@ public void setUp() throws Exception { .build(); TestComponent.useTestModule( - this, - TestModule.builder() - .withCustomBuildApksCommandSetter(command -> command.setEnableNewApkSerializer(true)) - .withSigningConfig(apkSigningConfig) - .build()); + this, TestModule.builder().withSigningConfig(apkSigningConfig).build()); bundlePath = tmpDir.resolve("bundle.aab"); apkZipPath = tmpDir.resolve("apks.zip"); @@ -803,7 +799,7 @@ public void apkMode_transparencyVerified_unsupportedCodeTransparencyVersion() th CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) .build()) .build(); - apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + apkSerializer.serialize(apkPath, baseModuleSplit); ZipBuilder zipBuilder = new ZipBuilder() .addFileWithContent( @@ -866,7 +862,7 @@ public void apkMode_transparencyVerified_transparencyKeyCertificateNotProvidedBy CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) .build()) .build(); - apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + apkSerializer.serialize(apkPath, baseModuleSplit); ZipBuilder zipBuilder = new ZipBuilder() .addFileWithContent( @@ -941,7 +937,7 @@ public void apkMode_transparencyVerified_apkSigningKeyCertificateProvidedByUser( CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) .build()) .build(); - apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + apkSerializer.serialize(apkPath, baseModuleSplit); ZipBuilder zipBuilder = new ZipBuilder() .addFileWithContent( @@ -1013,7 +1009,7 @@ public void apkMode_transparencyVerified_unspecifiedTypeForDexFiles() throws Exc CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) .build()) .build(); - apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + apkSerializer.serialize(apkPath, baseModuleSplit); ZipBuilder zipBuilder = new ZipBuilder() .addFileWithContent( @@ -1087,7 +1083,7 @@ public void apkMode_verificationFailed_apkSigningKeyCertificateMismatch() throws CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) .build()) .build(); - apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + apkSerializer.serialize(apkPath, baseModuleSplit); ZipBuilder zipBuilder = new ZipBuilder() .addFileWithContent( diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java index 66ef384f..b0434398 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java @@ -86,6 +86,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.Versions; import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleConfigBuilder; @@ -309,7 +310,7 @@ public void mergeSingleShard_throwsIfConflictingAssets() throws Exception { Throwable exception = assertThrows( - IllegalStateException.class, + InvalidBundleException.class, () -> splitsToShardMerger.mergeSingleShard( ImmutableList.of(split1, split2), createCache())); @@ -424,7 +425,7 @@ public void dexFiles_inMultipleModules_areRenamedForLPlus() throws Exception { ModuleSplit featureSplit = createModuleSplitBuilder() .setModuleName(BundleModuleName.create("feature")) - .setEntries(ImmutableList.of(dexEntry2, dexEntry3)) + .setEntries(ImmutableList.of(dexEntry3, dexEntry2)) .build(); ModuleSplit merged = @@ -432,7 +433,8 @@ public void dexFiles_inMultipleModules_areRenamedForLPlus() throws Exception { ImmutableList.of(baseSplit, featureSplit), dexMergingCache); assertThat(extractPaths(merged.getEntries())) - .containsExactly("dex/classes.dex", "dex/classes2.dex", "dex/classes3.dex"); + .containsExactly("dex/classes.dex", "dex/classes2.dex", "dex/classes3.dex") + .inOrder(); assertThat(dexData(merged, "dex/classes.dex")).isEqualTo(CLASSES_DEX_CONTENT); assertThat(dexData(merged, "dex/classes2.dex")).isEqualTo(CLASSES_OTHER_DEX_CONTENT); assertThat(dexData(merged, "dex/classes3.dex")).isEqualTo(CLASSES_DEX_CONTENT); diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java index fd781c80..0a662454 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java @@ -46,6 +46,7 @@ import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -445,7 +446,7 @@ public void assetsConfigMerging_throwsIfConflictingAssets() throws Exception { Throwable exception = assertThrows( - IllegalStateException.class, + InvalidBundleException.class, () -> new SameTargetingMerger().merge(ImmutableList.of(split1, split2))); assertThat(exception) diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index f254792e..92dc6cdc 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -40,9 +40,13 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.IS_GAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MODULE_TYPE_ASSET_VALUE; import static com.android.tools.build.bundletool.model.AndroidManifest.MODULE_TYPE_FEATURE_VALUE; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RESOURCE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_RESOURCE_ID; @@ -95,6 +99,7 @@ import com.android.tools.build.bundletool.TestData; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.xmlproto.UnexpectedAttributeTypeException; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.model.version.Version; import com.google.common.collect.ImmutableList; @@ -1434,6 +1439,85 @@ public void hasBackupAgent_missing_returnsFalse() { assertThat(androidManifest.hasBackupAgent()).isFalse(); } + @Test + public void getPermissions_success() { + XmlProtoElement permission1 = + new XmlProtoElement( + xmlElement( + PERMISSION_ELEMENT_NAME, + xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "SEND_SMS"))); + XmlProtoElement permission2 = + new XmlProtoElement( + xmlElement( + PERMISSION_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, + "name", + NAME_RESOURCE_ID, + "com.some.other.PERMISSION"), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, ICON_ATTRIBUTE_NAME, ICON_RESOURCE_ID, 12341234), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, LABEL_ATTRIBUTE_NAME, LABEL_RESOURCE_ID, 0x12345678), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, + DESCRIPTION_ATTRIBUTE_NAME, + DESCRIPTION_RESOURCE_ID, + 0x87654321), + xmlAttribute(ANDROID_NAMESPACE_URI, "protectionLevel", "normal|signature"), + xmlAttribute(ANDROID_NAMESPACE_URI, "permissionGroup", "group1")))); + AndroidManifest androidManifest = + AndroidManifest.create( + xmlNode( + xmlElement( + "manifest", xmlNode(permission1.getProto()), xmlNode(permission2.getProto())))); + + assertThat(androidManifest.getPermissions()).containsExactly(permission1, permission2); + } + + @Test + public void getPermissionGroups_success() { + XmlProtoElement permisisonGroup1 = + new XmlProtoElement( + xmlElement( + PERMISSION_GROUP_ELEMENT_NAME, + xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "group.name.1"))); + XmlProtoElement permisisonGroup2 = + new XmlProtoElement( + xmlElement( + PERMISSION_GROUP_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "group.name.2"), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, ICON_ATTRIBUTE_NAME, ICON_RESOURCE_ID, 12341234), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, LABEL_ATTRIBUTE_NAME, LABEL_RESOURCE_ID, 0x12345678), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, + DESCRIPTION_ATTRIBUTE_NAME, + DESCRIPTION_RESOURCE_ID, + 0x87654321)))); + AndroidManifest androidManifest = + AndroidManifest.create( + xmlNode( + xmlElement( + "manifest", + xmlNode(permisisonGroup1.getProto()), + xmlNode(permisisonGroup2.getProto())))); + + assertThat(androidManifest.getPermissionGroups()) + .containsExactly(permisisonGroup1, permisisonGroup2); + } + + @Test + public void getPermissions_isEmpty() { + AndroidManifest androidManifest = + AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(xmlElement("application"))))); + + assertThat(androidManifest.getPermissionGroups()).isEmpty(); + } + private AndroidManifest createManifestWithApplicationAttribute( String name, int resourceId, String value) { return AndroidManifest.create( @@ -1470,4 +1554,21 @@ private AndroidManifest createManifestWithApplicationRefIdAttribute( xmlResourceReferenceAttribute( ANDROID_NAMESPACE_URI, name, resourceId, value)))))); } + + @Test + public void hasLocaleConfig_missing_returnsFalse() { + AndroidManifest androidManifest = + AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(xmlElement("application"))))); + + assertThat(androidManifest.hasLocaleConfig()).isFalse(); + } + + @Test + public void hasLocaleConfig_present() { + AndroidManifest androidManifest = + createManifestWithApplicationRefIdAttribute( + LOCALE_CONFIG_ATTRIBUTE_NAME, LOCALE_CONFIG_RESOURCE_ID, 0x12345678); + + assertThat(androidManifest.hasLocaleConfig()).isTrue(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java index 653b66cf..e65d1314 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java @@ -52,6 +52,8 @@ import com.android.bundle.Files.TargetedApexImage; import com.android.bundle.Files.TargetedAssetsDirectory; import com.android.bundle.Files.TargetedNativeDirectory; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.ModuleTargeting; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.testing.BundleConfigBuilder; @@ -536,6 +538,47 @@ public void moduleTargeting_noConditions_noMinSdkInherited() { assertThat(moduleTargeting).isEqualToDefaultInstance(); } + @Test + public void missingRuntimeEnabledSdkConfigFile_returnsEmptyProto() { + BundleModule bundleModule = createMinimalModuleBuilder().build(); + + assertThat(bundleModule.getRuntimeEnabledSdkConfig()).isEmpty(); + } + + @Test + public void correctRuntimeEnabledSdkConfigFile_parsedAndReturned() { + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig = + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("sdk.package.name") + .setVersionMajor(1234) + .setCertificateDigest("AA:BB:CC:DD")) + .build(); + + BundleModule bundleModule = + createMinimalModuleBuilder() + .addEntry( + createModuleEntryForFile( + "runtime_enabled_sdk_config.pb", runtimeEnabledSdkConfig.toByteArray())) + .build(); + + assertThat(bundleModule.getRuntimeEnabledSdkConfig()).hasValue(runtimeEnabledSdkConfig); + } + + @Test + public void incorrectRuntimeEnabledSdkConfigFile_throws() { + byte[] badRuntimeEnabledSdkConfig = new byte[] {'b', 'a', 'd'}; + + assertThrows( + UncheckedIOException.class, + () -> + createMinimalModuleBuilder() + .addEntry( + createModuleEntryForFile( + "runtime_enabled_sdk_config.pb", badRuntimeEnabledSdkConfig))); + } + private static BundleModule.Builder createMinimalModuleBuilder() { return BundleModule.builder() .setName(BundleModuleName.create("testModule")) diff --git a/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java b/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java index 09eab55f..943edf7e 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java @@ -39,7 +39,7 @@ public void builder_empty() { assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); assertThat(generatedApks.getSystemApks()).isEmpty(); - assertThat(generatedApks.getHibernatedApks()).isEmpty(); + assertThat(generatedApks.getArchivedApks()).isEmpty(); } @Test @@ -51,7 +51,7 @@ public void fromModuleSplits_empty() { assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); assertThat(generatedApks.getSystemApks()).isEmpty(); - assertThat(generatedApks.getHibernatedApks()).isEmpty(); + assertThat(generatedApks.getArchivedApks()).isEmpty(); } @Test @@ -68,7 +68,7 @@ public void fromModuleSplits_correctSizes() { assertThat(generatedApks.getStandaloneApks()).containsExactly(standaloneApk); assertThat(generatedApks.getInstantApks()).containsExactly(instantApk); assertThat(generatedApks.getSystemApks()).isEmpty(); - assertThat(generatedApks.getHibernatedApks()).isEmpty(); + assertThat(generatedApks.getArchivedApks()).isEmpty(); } @Test @@ -82,21 +82,21 @@ public void fromModuleSplits_withSystemSplits_correctSizes() { assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); assertThat(generatedApks.getSystemApks()).containsExactly(systemApk); - assertThat(generatedApks.getHibernatedApks()).isEmpty(); + assertThat(generatedApks.getArchivedApks()).isEmpty(); } @Test - public void fromModuleSplits_withHibernatedApk_correctSizes() { - ModuleSplit hibernatedApk = createModuleSplit(SplitType.HIBERNATION); + public void fromModuleSplits_withArchivedApk_correctSizes() { + ModuleSplit archivedApk = createModuleSplit(SplitType.ARCHIVE); - GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(ImmutableList.of(hibernatedApk)); + GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(ImmutableList.of(archivedApk)); assertThat(generatedApks.size()).isEqualTo(1); assertThat(generatedApks.getSplitApks()).isEmpty(); assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); assertThat(generatedApks.getSystemApks()).isEmpty(); - assertThat(generatedApks.getHibernatedApks()).containsExactly(hibernatedApk); + assertThat(generatedApks.getArchivedApks()).containsExactly(archivedApk); } private static ModuleSplit createModuleSplit(SplitType splitType) { diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index 3a95b6b8..0adbb899 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -39,6 +39,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LABEL_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MAX_SDK_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MIN_SDK_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_ATTRIBUTE_NAME; @@ -846,6 +848,21 @@ public void setLabelAsRefId() throws Exception { ANDROID_NAMESPACE_URI, LABEL_ATTRIBUTE_NAME, LABEL_RESOURCE_ID, 0x12345678)); } + @Test + public void setLocaleConfig() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = androidManifest.toEditor().setLocaleConfig(0x12345678).save(); + + assertThat(getApplicationElement(editedManifest).getAttributeList()) + .containsExactly( + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, + LOCALE_CONFIG_ATTRIBUTE_NAME, + LOCALE_CONFIG_RESOURCE_ID, + 0x12345678)); + } + @Test public void setIcon() throws Exception { AndroidManifest androidManifest = createManifestWithApplicationElement(); @@ -946,6 +963,82 @@ public void addReceiver() throws Exception { .containsExactly(receiverXmlNode); } + @Test + public void copyPermissions() throws Exception { + XmlElement permisisonElement = + xmlElement( + "permission", + xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "SEND_SMS")); + AndroidManifest manifestWithPermissions = + AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(permisisonElement)))); + AndroidManifest manifestToUpdate = AndroidManifest.create(androidManifest("com.test.app")); + + AndroidManifest updatedManifest = + manifestToUpdate.toEditor().copyPermissions(manifestWithPermissions).save(); + + ImmutableList copiedPermissions = + updatedManifest.getManifestRoot().getProto().getElement().getChildList().stream() + .map(XmlNode::getElement) + .filter(childElement -> childElement.getName().equals("permission")) + .collect(toImmutableList()); + assertThat(copiedPermissions).containsExactly(permisisonElement); + } + + @Test + public void copyPermissions_noPermissions() throws Exception { + AndroidManifest manifestWithoutPermissions = + AndroidManifest.create(xmlNode(xmlElement("manifest"))); + AndroidManifest manifestToUpdate = AndroidManifest.create(androidManifest("com.test.app")); + + AndroidManifest updatedManifest = + manifestToUpdate.toEditor().copyPermissions(manifestWithoutPermissions).save(); + + ImmutableList copiedPermissions = + updatedManifest.getManifestRoot().getProto().getElement().getChildList().stream() + .map(XmlNode::getElement) + .filter(childElement -> childElement.getName().equals("permission")) + .collect(toImmutableList()); + assertThat(copiedPermissions).isEmpty(); + } + + @Test + public void copyPermissionGroups() throws Exception { + XmlElement permisisonGroupElement = + xmlElement( + "permission-group", + xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "group.1")); + AndroidManifest manifestWithPermissionGroups = + AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(permisisonGroupElement)))); + AndroidManifest manifestToUpdate = AndroidManifest.create(androidManifest("com.test.app")); + + AndroidManifest updatedManifest = + manifestToUpdate.toEditor().copyPermissionGroups(manifestWithPermissionGroups).save(); + + ImmutableList copiedPermissionGroups = + updatedManifest.getManifestRoot().getProto().getElement().getChildList().stream() + .map(XmlNode::getElement) + .filter(childElement -> childElement.getName().equals("permission-group")) + .collect(toImmutableList()); + assertThat(copiedPermissionGroups).containsExactly(permisisonGroupElement); + } + + @Test + public void copyPermissionGroups_noPermissionGroups() throws Exception { + AndroidManifest manifestWithoutPermissionGroups = + AndroidManifest.create(xmlNode(xmlElement("manifest"))); + AndroidManifest manifestToUpdate = AndroidManifest.create(androidManifest("com.test.app")); + + AndroidManifest updatedManifest = + manifestToUpdate.toEditor().copyPermissionGroups(manifestWithoutPermissionGroups).save(); + + ImmutableList copiedPermissionGroups = + updatedManifest.getManifestRoot().getProto().getElement().getChildList().stream() + .map(XmlNode::getElement) + .filter(childElement -> childElement.getName().equals("permission-group")) + .collect(toImmutableList()); + assertThat(copiedPermissionGroups).isEmpty(); + } + private static void assertOnlyMetadataElement( AndroidManifest manifest, String name, XmlAttribute valueAttr) { XmlElement applicationElement = getApplicationElement(manifest); diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleEntryTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleEntryTest.java index 1706f260..996bd7f6 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleEntryTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleEntryTest.java @@ -19,6 +19,9 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.io.ByteSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -82,7 +85,42 @@ public void equals_sameFiles() throws Exception { assertThat(entry.equals(entry)).isTrue(); } + @Test + public void equals_ensureEntryContentIsReadOnce() throws Exception { + ZipPath zipPath = ZipPath.create("a"); + CountingByteSource content1 = new CountingByteSource(new byte[] {'a'}); + CountingByteSource content2 = new CountingByteSource(new byte[] {'a'}); + + ModuleEntry entry1 = ModuleEntry.builder().setPath(zipPath).setContent(content1).build(); + ModuleEntry entry2 = ModuleEntry.builder().setPath(zipPath).setContent(content2).build(); + + for (int i = 0; i < 10; i++) { + assertThat(entry1.equals(entry2)).isTrue(); + } + assertThat(content1.getOpenStreamCount()).isEqualTo(1); + assertThat(content2.getOpenStreamCount()).isEqualTo(1); + } + private static ModuleEntry createEntry(ZipPath path, byte[] content) throws Exception { return ModuleEntry.builder().setPath(path).setContent(ByteSource.wrap(content)).build(); } + + private static class CountingByteSource extends ByteSource { + private final byte[] content; + private int count; + + CountingByteSource(byte[] content) { + this.content = content; + } + + int getOpenStreamCount() { + return count; + } + + @Override + public InputStream openStream() throws IOException { + count++; + return new ByteArrayInputStream(content); + } + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java index 122c97ff..500fdcbf 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -471,25 +471,25 @@ public void testStampSource_invalidUrl() throws Exception { } @Test - public void forHibernation() throws Exception { + public void forArchive() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) .addFile("res/drawable/background.jpg", DUMMY_CONTENT) .build(); - AndroidManifest hibernatedManifest = + AndroidManifest archivedManifest = AndroidManifest.create( androidManifest("com.test.app", ManifestProtoUtils.withVersionCode(123))); - Path hibernatedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); ModuleSplit split = - ModuleSplit.forHibernation( + ModuleSplit.forArchive( module, - hibernatedManifest, - /* hibernatedResourceTable= */ Optional.empty(), - hibernatedClassesDexFile); + archivedManifest, + /* archivedResourceTable= */ Optional.empty(), + archivedClassesDexFile); - assertThat(split.getSplitType()).isEqualTo(SplitType.HIBERNATION); + assertThat(split.getSplitType()).isEqualTo(SplitType.ARCHIVE); assertThat(split.getModuleName().getName()).isEqualTo("testModule"); assertThat(split.getAndroidManifest().getVersionCode()).hasValue(123); assertThat(split.getResourceTable()).isEmpty(); @@ -497,87 +497,81 @@ public void forHibernation() throws Exception { } @Test - public void forHibernation_copiesResourceTable() throws Exception { + public void forArchive_copiesResourceTable() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) .addFile("res/drawable/icon.jpg", DUMMY_CONTENT) .build(); - AndroidManifest hibernatedManifest = + AndroidManifest archivedManifest = AndroidManifest.create( androidManifest("com.test.app", ManifestProtoUtils.withVersionCode(123))); - ResourceTable hibernatedResourceTable = + ResourceTable archivedResourceTable = new ResourceTableBuilder() .addPackage("com.test.app") .addDrawableResource("icon", "res/drawable/icon.jpg") .build(); - Path hibernatedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); ModuleSplit split = - ModuleSplit.forHibernation( - module, - hibernatedManifest, - Optional.of(hibernatedResourceTable), - hibernatedClassesDexFile); + ModuleSplit.forArchive( + module, archivedManifest, Optional.of(archivedResourceTable), archivedClassesDexFile); - assertThat(split.getResourceTable().get()).isEqualTo(hibernatedResourceTable); + assertThat(split.getResourceTable().get()).isEqualTo(archivedResourceTable); assertThat(extractPaths(split.getEntries())) .containsExactly("res/drawable/icon.jpg", "dex/classes.dex"); } @Test - public void forHibernation_filtersResources() throws Exception { + public void forArchive_filtersResources() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) .addFile("res/drawable/icon.jpg", DUMMY_CONTENT) .addFile("res/drawable/background.jpg", DUMMY_CONTENT) .build(); - AndroidManifest hibernatedManifest = + AndroidManifest archivedManifest = AndroidManifest.create( androidManifest("com.test.app", ManifestProtoUtils.withVersionCode(123))); - ResourceTable hibernatedResourceTable = + ResourceTable archivedResourceTable = new ResourceTableBuilder() .addPackage("com.test.app") .addDrawableResource("icon", "res/drawable/icon.jpg") .build(); - Path hibernatedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); ModuleSplit split = - ModuleSplit.forHibernation( - module, - hibernatedManifest, - Optional.of(hibernatedResourceTable), - hibernatedClassesDexFile); + ModuleSplit.forArchive( + module, archivedManifest, Optional.of(archivedResourceTable), archivedClassesDexFile); - assertThat(split.getResourceTable().get()).isEqualTo(hibernatedResourceTable); + assertThat(split.getResourceTable().get()).isEqualTo(archivedResourceTable); assertThat(extractPaths(split.getEntries())) .containsExactly("res/drawable/icon.jpg", "dex/classes.dex"); } @Test - public void forHibernation_usesHibernatedClassesFile() throws Exception { + public void forArchive_usesArchivedClassesFile() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) .addFile("dex/classes.dex", DUMMY_CONTENT) .build(); - AndroidManifest hibernatedManifest = + AndroidManifest archivedManifest = AndroidManifest.create( androidManifest("com.test.app", ManifestProtoUtils.withVersionCode(123))); - byte[] hibernatedClassesDexFileContent = {1, 2}; - Path hibernatedClassesDexFile = createTempClassesDexFile(hibernatedClassesDexFileContent); + byte[] archivedClassesDexFileContent = {1, 2}; + Path archivedClassesDexFile = createTempClassesDexFile(archivedClassesDexFileContent); ModuleSplit split = - ModuleSplit.forHibernation( + ModuleSplit.forArchive( module, - hibernatedManifest, - /* hibernatedResourceTable= */ Optional.empty(), - hibernatedClassesDexFile); + archivedManifest, + /* archivedResourceTable= */ Optional.empty(), + archivedClassesDexFile); assertThat(extractPaths(split.getEntries())).containsExactly("dex/classes.dex"); assertThat(split.getEntries().get(0).getContent().read()) - .isEqualTo(hibernatedClassesDexFileContent); + .isEqualTo(archivedClassesDexFileContent); } private ImmutableList fakeEntriesOf(String... entries) { diff --git a/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java new file mode 100644 index 00000000..0a58effc --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.testing.BundleConfigBuilder; +import java.nio.file.Path; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the SdkBundle class. */ +@RunWith(JUnit4.class) +public class SdkBundleTest { + + private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final String PACKAGE_NAME = "com.test.sdk.detail"; + private static final BundleConfig BUNDLE_CONFIG = BundleConfigBuilder.create().build(); + public static final XmlNode MANIFEST = androidManifest(PACKAGE_NAME); + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + private Path bundleFile; + + @Before + public void setUp() { + bundleFile = tmp.getRoot().toPath().resolve("bundle.asb"); + } + + @Test + public void buildFromZipDoesNotCrash() throws Exception { + + createBasicZipBuilderWithManifest().writeTo(bundleFile); + + try (ZipFile sdkBundleZip = new ZipFile(bundleFile.toFile())) { + SdkBundle.buildFromZip(sdkBundleZip, 1); + } + } + + @Test + public void buildFromZipCreatesExpectedEntries() throws Exception { + createBasicZipBuilderWithManifest() + .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), DUMMY_CONTENT) + .writeTo(bundleFile); + + try (ZipFile sdkBundleZip = new ZipFile(bundleFile.toFile())) { + SdkBundle sdkBundle = SdkBundle.buildFromZip(sdkBundleZip, 1); + assertThat(sdkBundle.getModule().getEntry(ZipPath.create("dex/classes.dex"))).isPresent(); + assertThat(sdkBundle.getModule().getEntry(ZipPath.create("dex/classes2.dex"))).isPresent(); + assertThat(sdkBundle.getBundleMetadata().getFileAsByteSource("some.namespace", "metadata1")) + .isPresent(); + } + } + + @Test + public void buildFromZip_aarNotLoadedAsModule() throws Exception { + createBasicZipBuilderWithManifest() + .addFileWithContent(ZipPath.create("aar/library.aar"), DUMMY_CONTENT) + .writeTo(bundleFile); + + try (ZipFile sdkBundleZip = new ZipFile(bundleFile.toFile())) { + SdkBundle sdkBundle = SdkBundle.buildFromZip(sdkBundleZip, 1); + assertThat(sdkBundle.getModule().getName().toString()).isEqualTo("base"); + } + } + + private ZipBuilder createBasicZipBuilderWithManifest() { + ZipBuilder zipBuilder = new ZipBuilder(); + zipBuilder + .addFileWithContent(ZipPath.create("BundleConfig.pb"), BUNDLE_CONFIG.toByteArray()) + .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent( + ZipPath.create("BUNDLE-METADATA/some.namespace/metadata1"), new byte[] {0x01}); + return zipBuilder; + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java index a2640116..da79761b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java @@ -218,18 +218,17 @@ emptySdkTargeting, variantDensityTargeting(DensityAlias.LDPI)), assertThat(processedApks.getInstantApks()).isEmpty(); assertThat(processedApks.getStandaloneApks()).isEmpty(); assertThat(processedApks.getSplitApks()).isEmpty(); - assertThat(processedApks.getHibernatedApks()).isEmpty(); + assertThat(processedApks.getArchivedApks()).isEmpty(); assertThat(processedApks.getSystemApks()).isEqualTo(systemSplits); } @Test - public void hibernatedApksPassThrough() { + public void archivedApksPassThrough() { VariantTargeting emptySdkTargeting = variantSdkTargeting(SdkVersion.getDefaultInstance()); - ImmutableList hibernatedSplits = - ImmutableList.of(createModuleSplit(emptySdkTargeting, SplitType.HIBERNATION)); + ImmutableList archivedSplits = + ImmutableList.of(createModuleSplit(emptySdkTargeting, SplitType.ARCHIVE)); - GeneratedApks generatedApks = - GeneratedApks.builder().setHibernatedApks(hibernatedSplits).build(); + GeneratedApks generatedApks = GeneratedApks.builder().setArchivedApks(archivedSplits).build(); GeneratedApks processedApks = AlternativeVariantTargetingPopulator.populateAlternativeVariantTargeting(generatedApks); @@ -239,7 +238,7 @@ public void hibernatedApksPassThrough() { assertThat(processedApks.getStandaloneApks()).isEmpty(); assertThat(processedApks.getSplitApks()).isEmpty(); assertThat(processedApks.getSystemApks()).isEmpty(); - assertThat(processedApks.getHibernatedApks()).isEqualTo(hibernatedSplits); + assertThat(processedApks.getArchivedApks()).isEqualTo(archivedSplits); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjectorTest.java new file mode 100644 index 00000000..0eff9bb6 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjectorTest.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.model.utils; + +import static com.android.tools.build.bundletool.model.BundleModuleName.BASE_MODULE_NAME; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SPLIT; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SYSTEM; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; +import static com.android.tools.build.bundletool.testing.truth.resources.TruthResourceTable.assertThat; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.MoreCollectors.onlyElement; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.model.VariantKey; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; +import com.android.tools.build.bundletool.testing.ResourceTableBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ExtensionRegistry; +import java.util.Arrays; +import java.util.HashSet; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.theories.Theories; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class LocaleConfigXmlInjectorTest { + private static final String PACKAGE_NAME = "com.example.app.module"; + private LocaleConfigXmlInjector localeConfigXmlInjector; + + @Before + public void setUp() { + localeConfigXmlInjector = new LocaleConfigXmlInjector(); + } + + @Test + public void process() throws Exception { + ModuleSplit baseMasterSplit = + createModuleSplit( + new ResourceTableBuilder().addPackage("com.example.app.module").build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ true, + SPLIT, + /* languageTargeting= */ null); + ModuleSplit otherSplitA = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("ru-RU", "module ru-RU")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + languageTargeting("ru")); + ModuleSplit otherSplitB = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales("module", ImmutableMap.of("fr", "module fr")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + languageTargeting("fr")); + ModuleSplit otherSplitC = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("en-AU", "module en-AU")) + .build(), + "module", + /* masterSplit= */ false, + SPLIT, + languageTargeting("en")); + ImmutableList otherSplits = + ImmutableList.of(otherSplitA, otherSplitB, otherSplitC); + + ImmutableList processedSplits = + localeConfigXmlInjector.process( + VariantKey.create(baseMasterSplit), + ImmutableList.builder().add(baseMasterSplit).addAll(otherSplits).build()); + + ModuleSplit processedBaseMasterSplit = + processedSplits.stream().filter(ModuleSplit::isMasterSplit).collect(onlyElement()); + + assertThat(processedBaseMasterSplit.getAndroidManifest().hasLocaleConfig()).isTrue(); + + assertThat(processedBaseMasterSplit.getResourceTable().get()) + .containsResource("com.example.app.module:xml/locales_config") + .withFileReference("res/xml/locales_config.xml"); + + XmlNode expectedLocaleConfigProtoXml = + createLocalesXmlNode(new HashSet<>(Arrays.asList("ru-RU", "fr"))); + + assertThat( + XmlNode.parseFrom( + processedBaseMasterSplit.getEntries().get(0).getContent().read(), + ExtensionRegistry.getEmptyRegistry())) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedLocaleConfigProtoXml); + + ImmutableList processedOtherSplits = + processedSplits.stream().filter(split -> !split.isMasterSplit()).collect(toImmutableList()); + assertThat(processedOtherSplits).containsExactly(otherSplitA, otherSplitB, otherSplitC); + } + + @Test + public void process_noLanguageTargeting() throws Exception { + ModuleSplit baseMasterSplit = + createModuleSplit( + new ResourceTableBuilder().addPackage("com.example.app.module").build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ true, + SPLIT, + /* languageTargeting= */ null); + ModuleSplit otherSplit = + createModuleSplit( + new ResourceTableBuilder().addPackage("com.example.app.module").build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + /* languageTargeting= */ null); + ModuleSplit processedBaseMasterSplit = + localeConfigXmlInjector + .process( + VariantKey.create(baseMasterSplit), ImmutableList.of(baseMasterSplit, otherSplit)) + .stream() + .filter(split -> split.isMasterSplit() && split.isBaseModuleSplit()) + .collect(onlyElement()); + + assertThat(processedBaseMasterSplit.getAndroidManifest().hasLocaleConfig()).isFalse(); + + assertThat(processedBaseMasterSplit.getResourceTable().get()) + .doesNotContainResource("com.example.app.module:xml/locales_config"); + } + + @Test + public void process_duplicateLocales() throws Exception { + ModuleSplit baseMasterSplit = + createModuleSplit( + new ResourceTableBuilder().addPackage("com.example.app.module").build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ true, + SPLIT, + /* languageTargeting= */ null); + ImmutableList otherSplits = + ImmutableList.of( + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("ru-RU", "module ru-RU")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + languageTargeting("ru")), + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("ru-RU", "module ru-RU")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + languageTargeting("ru")), + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("fr", "module fr")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SPLIT, + languageTargeting("fr"))); + + ModuleSplit processedBaseMasterSplit = + localeConfigXmlInjector + .process( + VariantKey.create(baseMasterSplit), + ImmutableList.builder() + .add(baseMasterSplit) + .addAll(otherSplits) + .build()) + .stream() + .filter(split -> split.isMasterSplit() && split.isBaseModuleSplit()) + .collect(onlyElement()); + XmlNode expectedLocaleConfigProtoXml = + createLocalesXmlNode(new HashSet<>(Arrays.asList("ru-RU", "fr"))); + + assertThat( + XmlNode.parseFrom( + processedBaseMasterSplit.getEntries().get(0).getContent().read(), + ExtensionRegistry.getEmptyRegistry())) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedLocaleConfigProtoXml); + } + + @Test + public void process_systemSplits() throws Exception { + ModuleSplit baseMasterSplit = + createModuleSplit( + new ResourceTableBuilder().addPackage("com.example.app.module").build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ true, + SYSTEM, + /* languageTargeting= */ null); + ModuleSplit otherSplitA = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("ru-RU", "module ru-RU")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SYSTEM, + languageTargeting("ru")); + ModuleSplit otherSplitB = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales("module", ImmutableMap.of("fr", "module fr")) + .build(), + BASE_MODULE_NAME.getName(), + /* masterSplit= */ false, + SYSTEM, + languageTargeting("fr")); + ModuleSplit otherSplitC = + createModuleSplit( + new ResourceTableBuilder() + .addPackage("com.example.app.module") + .addStringResourceForMultipleLocales( + "module", ImmutableMap.of("en-AU", "module en-AU")) + .build(), + "module", + /* masterSplit= */ false, + SYSTEM, + languageTargeting("en")); + ImmutableList otherSplits = + ImmutableList.of(otherSplitA, otherSplitB, otherSplitC); + + ImmutableList processedSplits = + localeConfigXmlInjector.process( + VariantKey.create(baseMasterSplit), + ImmutableList.builder().add(baseMasterSplit).addAll(otherSplits).build()); + + ModuleSplit processedBaseMasterSplit = + processedSplits.stream().filter(ModuleSplit::isMasterSplit).collect(onlyElement()); + + assertThat(processedBaseMasterSplit.getAndroidManifest().hasLocaleConfig()).isTrue(); + + assertThat(processedBaseMasterSplit.getResourceTable().get()) + .containsResource("com.example.app.module:xml/locales_config") + .withFileReference("res/xml/locales_config.xml"); + + XmlNode expectedLocaleConfigProtoXml = + createLocalesXmlNode(new HashSet<>(Arrays.asList("ru-RU", "fr"))); + + assertThat( + XmlNode.parseFrom( + processedBaseMasterSplit.getEntries().get(0).getContent().read(), + ExtensionRegistry.getEmptyRegistry())) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedLocaleConfigProtoXml); + + ImmutableList processedOtherSplits = + processedSplits.stream().filter(split -> !split.isMasterSplit()).collect(toImmutableList()); + assertThat(processedOtherSplits).containsExactly(otherSplitA, otherSplitB, otherSplitC); + } + + private static ModuleSplit createModuleSplit( + ResourceTable resourceTable, + String moduleName, + boolean masterSplit, + SplitType splitType, + @Nullable LanguageTargeting languageTargeting) { + return ModuleSplit.builder() + .setAndroidManifest(AndroidManifest.create(androidManifest(PACKAGE_NAME))) + .setResourceTable(resourceTable) + .setEntries(ImmutableList.of()) + .setMasterSplit(masterSplit) + .setSplitType(splitType) + .setModuleName(BundleModuleName.create(moduleName)) + .setApkTargeting( + languageTargeting == null + ? ApkTargeting.getDefaultInstance() + : ApkTargeting.newBuilder().setLanguageTargeting(languageTargeting).build()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + } + + private static XmlNode createLocalesXmlNode(HashSet locales) { + XmlProtoElementBuilder localesConfigXml = XmlProtoElementBuilder.create("locale-config"); + + locales.forEach(locale -> localesConfigXml.addChildElement(createAttributes(locale))); + + return XmlProtoNode.createElementNode(localesConfigXml.build()).getProto(); + } + + private static XmlProtoElementBuilder createAttributes(String locale) { + return XmlProtoElementBuilder.create("locale") + .addAttribute(XmlProtoAttributeBuilder.create("android:name").setValueAsString(locale)); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java index 826c357a..6abc755e 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java @@ -175,11 +175,11 @@ public void filterSystemApkVariant() throws Exception { } @Test - public void filterHibernatedApkVariant() throws Exception { - Variant hibernatedVariant = createHibernatedVariant(); - BuildApksResult apksResult = BuildApksResult.newBuilder().addVariant(hibernatedVariant).build(); + public void filterArchivedApkVariant() throws Exception { + Variant archivedVariant = createArchivedVariant(); + BuildApksResult apksResult = BuildApksResult.newBuilder().addVariant(archivedVariant).build(); - assertThat(ResultUtils.hibernatedApkVariants(apksResult)).containsExactly(hibernatedVariant); + assertThat(ResultUtils.archivedApkVariants(apksResult)).containsExactly(archivedVariant); } @Test @@ -190,7 +190,7 @@ public void isInstantApkVariantTrue() throws Exception { assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); - assertThat(ResultUtils.isHibernatedApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isArchivedApkVariant(variant)).isFalse(); } @Test @@ -201,7 +201,7 @@ public void isStandaloneApkVariantTrue() throws Exception { assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); - assertThat(ResultUtils.isHibernatedApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isArchivedApkVariant(variant)).isFalse(); } @Test @@ -212,7 +212,7 @@ public void isSplitApkVariantTrue() throws Exception { assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); - assertThat(ResultUtils.isHibernatedApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isArchivedApkVariant(variant)).isFalse(); } @Test @@ -223,14 +223,14 @@ public void isSystemApkVariantTrue() throws Exception { assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); assertThat(ResultUtils.isSystemApkVariant(variant)).isTrue(); - assertThat(ResultUtils.isHibernatedApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isArchivedApkVariant(variant)).isFalse(); } @Test - public void isHibernatedApkVariantTrue() throws Exception { - Variant variant = createHibernatedVariant(); + public void isArchivedApkVariantTrue() throws Exception { + Variant variant = createArchivedVariant(); - assertThat(ResultUtils.isHibernatedApkVariant(variant)).isTrue(); + assertThat(ResultUtils.isArchivedApkVariant(variant)).isTrue(); assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); @@ -315,11 +315,10 @@ private Variant createSystemVariant() { createSystemApkSet(ApkTargeting.getDefaultInstance(), systemApk)); } - private Variant createHibernatedVariant() { - ZipPath hibernatedApk = ZipPath.create("hibernated.apk"); + private Variant createArchivedVariant() { + ZipPath archivedApk = ZipPath.create("archived.apk"); return createVariant( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(SdkVersion.getDefaultInstance())), - ApksArchiveHelpers.createHibernatedApkSet( - ApkTargeting.getDefaultInstance(), hibernatedApk)); + ApksArchiveHelpers.createArchivedApkSet(ApkTargeting.getDefaultInstance(), archivedApk)); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjectorTest.java index 4a6f755d..e55ccb4a 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjectorTest.java @@ -17,7 +17,7 @@ package com.android.tools.build.bundletool.model.utils; import static com.android.tools.build.bundletool.model.BundleModuleName.BASE_MODULE_NAME; -import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.HIBERNATION; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.ARCHIVE; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.INSTANT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SPLIT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.STANDALONE; @@ -26,6 +26,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.createModuleEntryForFile; import static com.android.tools.build.bundletool.testing.truth.resources.TruthResourceTable.assertThat; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; @@ -48,6 +49,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ExtensionRegistry; +import java.util.Collection; import java.util.Optional; import javax.annotation.Nullable; import org.junit.Before; @@ -98,11 +100,10 @@ public void process_ignoresWrongSplits() { BASE_MODULE_NAME.getName(), /* splitId= */ "", /* masterSplit= */ true, - HIBERNATION, + ARCHIVE, /* languageTargeting= */ null)); - assertThat( - splitsXmlInjector.process(GeneratedApks.fromModuleSplits(modules)).getAllApksStream()) + assertThat(xmlInjectorProcess(GeneratedApks.fromModuleSplits(modules)).stream()) .containsExactlyElementsIn(modules); } @@ -141,18 +142,13 @@ public void process() throws Exception { /* masterSplit= */ false, SPLIT, languageTargeting("ru"))); - GeneratedApks result = - splitsXmlInjector.process( - GeneratedApks.fromModuleSplits( - ImmutableList.builder() - .add(baseMasterSplit) - .addAll(otherSplits) - .build())); - - assertThat(result.getAllApksStream()).containsAtLeastElementsIn(otherSplits); + GeneratedApks generatedApks = + GeneratedApks.fromModuleSplits( + ImmutableList.builder().add(baseMasterSplit).addAll(otherSplits).build()); + + assertThat(generatedApks.getAllApksStream()).containsAtLeastElementsIn(otherSplits); ModuleSplit processedBaseMasterSplit = - result - .getAllApksStream() + xmlInjectorProcess(generatedApks).stream() .filter(module -> module.isMasterSplit() && module.isBaseModuleSplit()) .collect(onlyElement()); @@ -212,13 +208,11 @@ public void process_noLanguageTargeting() throws Exception { .setResourceTable(featureResourceTable) .build(); - GeneratedApks result = - splitsXmlInjector.process( - GeneratedApks.fromModuleSplits(ImmutableList.of(baseModule, featureModule))); + GeneratedApks generatedApks = + GeneratedApks.fromModuleSplits(ImmutableList.of(baseModule, featureModule)); ModuleSplit processedBaseMasterSplit = - result - .getAllApksStream() + xmlInjectorProcess(generatedApks).stream() .filter(module -> module.isMasterSplit() && module.isBaseModuleSplit()) .collect(onlyElement()); @@ -272,18 +266,13 @@ public void process_systemSplits() throws Exception { /* masterSplit= */ false, SYSTEM, languageTargeting("ru"))); - GeneratedApks result = - splitsXmlInjector.process( - GeneratedApks.fromModuleSplits( - ImmutableList.builder() - .add(baseMasterSplit) - .addAll(otherSplits) - .build())); - - assertThat(result.getAllApksStream()).containsAtLeastElementsIn(otherSplits); + GeneratedApks generatedApks = + GeneratedApks.fromModuleSplits( + ImmutableList.builder().add(baseMasterSplit).addAll(otherSplits).build()); + + assertThat(generatedApks.getAllApksStream()).containsAtLeastElementsIn(otherSplits); ModuleSplit processedBaseMasterSplit = - result - .getAllApksStream() + xmlInjectorProcess(generatedApks).stream() .filter(module -> module.isMasterSplit() && module.isBaseModuleSplit()) .collect(onlyElement()); @@ -325,10 +314,10 @@ public void process_standaloneSplitTypes() throws Exception { .build(); standalone = standalone.toBuilder().setResourceTable(standaloneResourceTable).build(); - GeneratedApks result = - splitsXmlInjector.process(GeneratedApks.fromModuleSplits(ImmutableList.of(standalone))); + GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(ImmutableList.of(standalone)); - ModuleSplit processedStandalone = result.getAllApksStream().collect(onlyElement()); + ModuleSplit processedStandalone = + xmlInjectorProcess(generatedApks).stream().collect(onlyElement()); assertThat( processedStandalone @@ -350,45 +339,32 @@ public void process_standaloneSplitTypes() throws Exception { } @Test - public void process_hibernationSplitTypes() throws Exception { - ModuleSplit hibernated = + public void process_archiveSplitTypes() throws Exception { + ModuleSplit archived = createModuleSplit( BASE_MODULE_NAME.getName(), /* splitId= */ "", /* masterSplit= */ true, - SplitType.HIBERNATION, + SplitType.ARCHIVE, /* languageTargeting= */ null); - ResourceTable hibernatedResourceTable = + ResourceTable archivedResourceTable = new ResourceTableBuilder() .addPackage("com.example.app") .addStringResourceForMultipleLocales( "title", ImmutableMap.of("ru-RU", "title ru-RU", "fr", "title fr")) .build(); - hibernated = hibernated.toBuilder().setResourceTable(hibernatedResourceTable).build(); + archived = archived.toBuilder().setResourceTable(archivedResourceTable).build(); - GeneratedApks result = - splitsXmlInjector.process(GeneratedApks.fromModuleSplits(ImmutableList.of(hibernated))); + GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(ImmutableList.of(archived)); - ModuleSplit processedStandalone = result.getAllApksStream().collect(onlyElement()); + ModuleSplit processedArchivedApk = + xmlInjectorProcess(generatedApks).stream().collect(onlyElement()); assertThat( - processedStandalone + processedArchivedApk .getAndroidManifest() .getMetadataResourceId("com.android.vending.splits")) - .hasValue(0x7f020000); - assertThat(processedStandalone.getResourceTable().get()) - .containsResource("com.example.app:xml/splits0") - .withFileReference("res/xml/splits0.xml"); - - XmlNode expectedSplitsProtoXml = - new SplitsProtoXmlBuilder() - .addLanguageMapping(BASE_MODULE_NAME, "ru", "") - .addLanguageMapping(BASE_MODULE_NAME, "fr", "") - .build(); - assertThat(XmlNode.parseFrom(processedStandalone.getEntries().get(0).getContent().read(), - ExtensionRegistry.getEmptyRegistry())) - .ignoringRepeatedFieldOrder() - .isEqualTo(expectedSplitsProtoXml); + .isEmpty(); } @Test @@ -408,9 +384,8 @@ public void process_fileExists() { .build(); ModuleSplit processedBaseMasterSplit = - splitsXmlInjector - .process(GeneratedApks.fromModuleSplits(ImmutableList.of(baseMasterSplit))) - .getAllApksStream() + xmlInjectorProcess(GeneratedApks.fromModuleSplits(ImmutableList.of(baseMasterSplit))) + .stream() .collect(onlyElement()); assertThat(processedBaseMasterSplit.getEntries()).hasSize(2); @@ -445,4 +420,11 @@ private static ModuleSplit createModuleSplit( .setVariantTargeting(VariantTargeting.getDefaultInstance()) .build(); } + + private ImmutableList xmlInjectorProcess(GeneratedApks generatedApks) { + return generatedApks.getAllApksGroupedByOrderedVariants().asMap().entrySet().stream() + .map(keySplit -> splitsXmlInjector.process(keySplit.getKey(), keySplit.getValue())) + .flatMap(Collection::stream) + .collect(toImmutableList()); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ApkSetUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/ApkSetUtils.java index bdfada7d..e0f8015b 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApkSetUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApkSetUtils.java @@ -18,6 +18,7 @@ import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Commands.BuildSdkApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.ModuleMetadata; import com.android.tools.build.bundletool.model.version.BundleToolVersion; @@ -57,6 +58,14 @@ public static File extractFromApkSetFile(ZipFile apkSetFile, String path, Path o return extractedFile; } + public static BuildSdkApksResult extractTocFromSdkApkSetFile( + ZipFile apkSetFile, Path outputDirPath) throws Exception { + File tocFile = extractFromApkSetFile(apkSetFile, "toc.pb", outputDirPath); + try (FileInputStream inputStream = new FileInputStream(tocFile)) { + return BuildSdkApksResult.parseFrom(inputStream); + } + } + public static ApkSet splitApkSet(String moduleName, ApkDescription... apkDescriptions) { return splitApkSet( moduleName, diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java index 338e0c0c..36cf1f9a 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java @@ -26,11 +26,11 @@ import com.android.bundle.Commands.ApexApkMetadata; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; +import com.android.bundle.Commands.ArchivedApkMetadata; import com.android.bundle.Commands.AssetModuleMetadata; import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; -import com.android.bundle.Commands.HibernatedApkMetadata; import com.android.bundle.Commands.ModuleMetadata; import com.android.bundle.Commands.SplitApkMetadata; import com.android.bundle.Commands.StandaloneApkMetadata; @@ -260,8 +260,8 @@ public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, ZipPath apkPa .build(); } - public static ApkSet createHibernatedApkSet(ApkTargeting apkTargeting, ZipPath apkPath) { - // Note: Hibernated APK is represented as a module named "base". + public static ApkSet createArchivedApkSet(ApkTargeting apkTargeting, ZipPath apkPath) { + // Note: Archived APK is represented as a module named "base". return ApkSet.newBuilder() .setModuleMetadata( ModuleMetadata.newBuilder().setName("base").setDeliveryType(DeliveryType.INSTALL_TIME)) @@ -269,7 +269,7 @@ public static ApkSet createHibernatedApkSet(ApkTargeting apkTargeting, ZipPath a ApkDescription.newBuilder() .setPath(apkPath.toString()) .setTargeting(apkTargeting) - .setHibernatedApkMetadata(HibernatedApkMetadata.getDefaultInstance())) + .setArchivedApkMetadata(ArchivedApkMetadata.getDefaultInstance())) .build(); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java index b12cdabd..ffc2c2e4 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java @@ -24,6 +24,7 @@ import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -128,6 +129,12 @@ public BundleModuleBuilder setApexConfig(ApexImages apexConfig) { return this; } + public BundleModuleBuilder setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig) { + addFile("runtime_enabled_sdk_config.pb", runtimeEnabledSdkConfig.toByteArray()); + return this; + } + public BundleModuleBuilder setManifest(XmlNode androidManifest) { this.androidManifest = androidManifest; return this; diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java index c8349658..96427a9a 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java @@ -44,9 +44,14 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.NATIVE_ACTIVITY_LIB_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.NO_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_TREE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RECEIVER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RESOURCE_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_MAJOR_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; @@ -950,6 +955,40 @@ public static ManifestMutator withNativeActivity(String libName) { .setValueAsString(libName)))); } + /** + * Adds an {@value + * com.android.tools.build.bundletool.model.AndroidManifest#SDK_LIBRARY_ELEMENT_NAME} element for + * SDK Bundle manifests. + */ + public static ManifestMutator withSdkLibraryElement(String majorVersion) { + return manifestElement -> + manifestElement + .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) + .getOrCreateChildElement(SDK_LIBRARY_ELEMENT_NAME) + .getOrCreateAttribute(ANDROID_NAMESPACE_URI, SDK_MAJOR_VERSION_ATTRIBUTE_NAME) + .setValueAsString(majorVersion); + } + + /** Adds a {@value #PERMISSION_ELEMENT_NAME} element to the manifest. */ + public static ManifestMutator withPermission() { + return manifestElement -> + manifestElement.addChildElement(XmlProtoElementBuilder.create(PERMISSION_ELEMENT_NAME)); + } + + /** Adds a {@value #PERMISSION_GROUP_ELEMENT_NAME} element to the manifest. */ + public static ManifestMutator withPermissionGroup() { + return manifestElement -> + manifestElement.addChildElement( + XmlProtoElementBuilder.create(PERMISSION_GROUP_ELEMENT_NAME)); + } + + /** Adds a {@value #PERMISSION_TREE_ELEMENT_NAME} element to the manifest. */ + public static ManifestMutator withPermissionTree() { + return manifestElement -> + manifestElement.addChildElement( + XmlProtoElementBuilder.create(PERMISSION_TREE_ELEMENT_NAME)); + } + /** Defined solely for readability. */ public interface ManifestMutator extends Consumer {} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java new file mode 100644 index 00000000..c05e09ee --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.testing; + +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkLibraryElement; + +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.SdkBundle; +import java.util.zip.ZipFile; + +/** + * Builder of {@link SdkBundle} instances in tests. + * + *

    Note that the SdkBundle is created in-memory. To test an SdkBundle created from a ZIP file, + * use {@link com.android.tools.build.bundletool.io.ZipBuilder} then call {@link + * SdkBundle#buildFromZip(ZipFile, Integer)}. + */ +public class SdkBundleBuilder { + + public static final BundleConfig DEFAULT_BUNDLE_CONFIG = BundleConfigBuilder.create().build(); + + public static final String PACKAGE_NAME = "com.test.sdk"; + + private static final BundleMetadata METADATA = BundleMetadata.builder().build(); + + private BundleModule module = defaultModule(); + + private Integer versionCode = 1; + + private static BundleModule defaultModule() { + return new BundleModuleBuilder("base").setManifest(createSdkAndroidManifest()).build(); + } + + private static XmlNode createSdkAndroidManifest() { + return androidManifest( + PACKAGE_NAME, + withMinSdkVersion(32), + withSdkLibraryElement("15"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "5")); + } + + public SdkBundleBuilder setModule(BundleModule module) { + this.module = module; + return this; + } + + public SdkBundleBuilder setVersionCode(Integer versionCode) { + this.versionCode = versionCode; + return this; + } + + public SdkBundle build() { + return SdkBundle.builder() + .setModule(module) + .setBundleConfig(DEFAULT_BUNDLE_CONFIG) + .setBundleMetadata(METADATA) + .setVersionCode(versionCode) + .build(); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java index f006f0f4..13769b45 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java @@ -26,13 +26,16 @@ import com.android.tools.build.bundletool.commands.BuildApksManagerComponent.UseBundleCompression; import com.android.tools.build.bundletool.commands.CommandScoped; import com.android.tools.build.bundletool.io.AppBundleSerializer; +import com.android.tools.build.bundletool.io.SdkBundleSerializer; import com.android.tools.build.bundletool.io.TempDirectory; import com.android.tools.build.bundletool.io.ZipReader; import com.android.tools.build.bundletool.model.ApkListener; import com.android.tools.build.bundletool.model.ApkModifier; import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.Bundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.SigningConfigurationProvider; import com.android.tools.build.bundletool.model.SourceStamp; @@ -54,19 +57,39 @@ public class TestModule { private final BuildApksCommand buildApksCommand; - private final AppBundle appBundle; + private final Bundle bundle; private final boolean useBundleCompression; private TestModule( - BuildApksCommand buildApksCommand, AppBundle appBundle, boolean useBundleCompression) { + BuildApksCommand buildApksCommand, Bundle bundle, boolean useBundleCompression) { this.buildApksCommand = buildApksCommand; - this.appBundle = appBundle; + this.bundle = bundle; this.useBundleCompression = useBundleCompression; } + @Provides + SdkBundle provideSdkBundle() { + return (SdkBundle) bundle; + } + @Provides AppBundle provideAppBundle() { - return appBundle; + return (AppBundle) bundle; + } + + @Provides + Bundle provideBundle() { + return bundle; + } + + @Provides + BundleConfig provideBundleConfig() { + return bundle.getBundleConfig(); + } + + @Provides + BundleMetadata provideBundleMetadata() { + return bundle.getBundleMetadata(); } @SuppressWarnings({"CloseableProvides", "MustBeClosedChecker"}) // Only for tests. @@ -110,7 +133,7 @@ public static class Builder { @Nullable private TempDirectory tempDirectory; @Nullable private Path outputDirectory; @Nullable private Path bundlePath; - @Nullable private AppBundle appBundle; + @Nullable private Bundle bundle; private BundleConfig bundleConfig = DEFAULT_BUNDLE_CONFIG; @Nullable private SigningConfiguration signingConfig; @Nullable private SigningConfigurationProvider signingConfigProvider; @@ -128,14 +151,15 @@ public static class Builder { @Nullable private Boolean localTestingEnabled; @Nullable private SourceStamp sourceStamp; private boolean useBundleCompression = true; + private boolean enableApkSerializerWithoutBundleRecompression = true; private BundleMetadata bundleMetadata = DEFAULT_BUNDLE_METADATA; public Builder withAppBundle(AppBundle appBundle) { - this.appBundle = appBundle; + this.bundle = appBundle; // If not set, set a default BundleConfig with the latest bundletool version. if (appBundle.getBundleConfig().equals(BundleConfig.getDefaultInstance())) { - this.appBundle = appBundle.toBuilder().setBundleConfig(DEFAULT_BUNDLE_CONFIG).build(); + this.bundle = appBundle.toBuilder().setBundleConfig(DEFAULT_BUNDLE_CONFIG).build(); } return this; } @@ -251,6 +275,16 @@ public Builder useBundleCompression(boolean useBundleCompression) { return this; } + public Builder withEnableApkSerializerWithoutBundleRecompression(boolean enable) { + this.enableApkSerializerWithoutBundleRecompression = enable; + return this; + } + + public Builder withSdkBundle(SdkBundle sdkBundle) { + this.bundle = sdkBundle; + return this; + } + public TestModule build() { try { if (tempDirectory == null) { @@ -261,13 +295,14 @@ public TestModule build() { } checkArgument( - appBundle == null || bundlePath == null, + bundle == null || bundlePath == null, "Cannot call both withAppBundle() and withBundlePath()."); - if (appBundle == null) { + if (bundle == null) { + // The default Bundle provided will be an AppBundle. if (bundlePath != null) { - appBundle = AppBundle.buildFromZip(new ZipFile(bundlePath.toFile())); + bundle = AppBundle.buildFromZip(new ZipFile(bundlePath.toFile())); } else { - appBundle = + bundle = new AppBundleBuilder() .setBundleConfig(bundleConfig) .addModule("base", module -> module.setManifest(androidManifest("com.package"))) @@ -276,16 +311,29 @@ public TestModule build() { } else { if (!bundleConfig.equals(DEFAULT_BUNDLE_CONFIG)) { BundleConfig newBundleConfig = - appBundle.getBundleConfig().toBuilder().mergeFrom(bundleConfig).build(); - appBundle = appBundle.toBuilder().setBundleConfig(newBundleConfig).build(); + bundle.getBundleConfig().toBuilder().mergeFrom(bundleConfig).build(); + if (bundle instanceof AppBundle) { + bundle = ((AppBundle) bundle).toBuilder().setBundleConfig(newBundleConfig).build(); + } else { + bundle = ((SdkBundle) bundle).toBuilder().setBundleConfig(newBundleConfig).build(); + } } } if (!bundleMetadata.equals(DEFAULT_BUNDLE_METADATA)) { - appBundle = appBundle.toBuilder().setBundleMetadata(bundleMetadata).build(); + if (bundle instanceof AppBundle) { + bundle = ((AppBundle) bundle).toBuilder().setBundleMetadata(bundleMetadata).build(); + } else { + bundle = ((SdkBundle) bundle).toBuilder().setBundleMetadata(bundleMetadata).build(); + } } if (bundlePath == null) { - bundlePath = tempDirectory.getPath().resolve("bundle.aab"); - new AppBundleSerializer().writeToDisk(appBundle, bundlePath); + if (bundle instanceof AppBundle) { + bundlePath = tempDirectory.getPath().resolve("bundle.aab"); + new AppBundleSerializer().writeToDisk((AppBundle) bundle, bundlePath); + } else { + bundlePath = tempDirectory.getPath().resolve("bundle.asb"); + new SdkBundleSerializer().writeToDisk((SdkBundle) bundle, bundlePath); + } } if (outputPath == null) { outputPath = outputDirectory.resolve("bundle.apks"); @@ -293,6 +341,8 @@ public TestModule build() { BuildApksCommand.Builder command = BuildApksCommand.builder() + .setEnableApkSerializerWithoutBundleRecompression( + enableApkSerializerWithoutBundleRecompression) .setAapt2Command(Aapt2Helper.getAapt2Command()) .setBundlePath(bundlePath) .setOutputFile(outputPath); @@ -339,7 +389,7 @@ public TestModule build() { buildApksCommandSetter.accept(command); } - return new TestModule(command.build(), appBundle, useBundleCompression); + return new TestModule(command.build(), bundle, useBundleCompression); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java index 52393365..6b34df51 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -20,11 +20,31 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.aapt.Resources.XmlNode; import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; +import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; +import com.google.protobuf.ExtensionRegistry; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -78,9 +98,80 @@ public static ImmutableList filesUnderPath(ZipFile zipFile, ZipPath path } public static ModuleEntry createModuleEntryForFile(String filePath, byte[] content) { + return createModuleEntryForFile(filePath, content, /* uncompressed= */ false); + } + + public static ModuleEntry createModuleEntryForFile( + String filePath, byte[] content, boolean uncompressed) { return ModuleEntry.builder() .setPath(ZipPath.create(filePath)) .setContent(ByteSource.wrap(content)) + .setForceUncompressed(uncompressed) .build(); } + + /** Extracts AndroidManifest from binary file in APK. */ + public static AndroidManifest extractAndroidManifest(File apk, Path tmpDir) { + Path protoApkPath = tmpDir.resolve("proto.apk"); + Aapt2Helper.convertBinaryApkToProtoApk(apk.toPath(), protoApkPath); + try { + try (ZipFile protoApk = new ZipFile(protoApkPath.toFile())) { + return AndroidManifest.create( + XmlNode.parseFrom( + protoApk.getInputStream(protoApk.getEntry("AndroidManifest.xml")), + ExtensionRegistry.getEmptyRegistry())); + } finally { + Files.deleteIfExists(protoApkPath); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** Creates a Java Keystore. */ + public static void createKeystore(Path keystorePath, String keystorePassword) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(/* stream= */ null, keystorePassword.toCharArray()); + try (FileOutputStream keystoreOutputStream = new FileOutputStream(keystorePath.toFile())) { + keystore.store(keystoreOutputStream, keystorePassword.toCharArray()); + } + } + + /** Adds a key to a Java Keystore. */ + public static void addKeyToKeystore( + Path keystorePath, + String keystorePassword, + String keyAlias, + String keyPassword, + PrivateKey privateKey, + X509Certificate certificate) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + KeyStore keystore = KeyStore.getInstance("JKS"); + try (FileInputStream keystoreInputStream = new FileInputStream(keystorePath.toFile())) { + keystore.load(keystoreInputStream, keystorePassword.toCharArray()); + } + keystore.setKeyEntry( + keyAlias, privateKey, keyPassword.toCharArray(), new Certificate[] {certificate}); + try (FileOutputStream keystoreOutputStream = new FileOutputStream(keystorePath.toFile())) { + keystore.store(keystoreOutputStream, keystorePassword.toCharArray()); + } + } + + /** Creates a Debug Keystore. */ + public static SigningConfiguration createDebugKeystore( + Path path, String keystorePassword, String keyAlias, String keyPassword) throws Exception { + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").genKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + X509Certificate certificate = + CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=Android Debug,O=Android,C=US"); + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(/* stream= */ null, keystorePassword.toCharArray()); + keystore.setKeyEntry( + keyAlias, privateKey, keyPassword.toCharArray(), new Certificate[] {certificate}); + try (FileOutputStream keystoreOutputStream = new FileOutputStream(path.toFile())) { + keystore.store(keystoreOutputStream, keystorePassword.toCharArray()); + } + return SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ZipAlignCheck.java b/src/test/java/com/android/tools/build/bundletool/testing/ZipAlignCheck.java index 4fe68921..85f8aa7f 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ZipAlignCheck.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ZipAlignCheck.java @@ -15,10 +15,8 @@ */ package com.android.tools.build.bundletool.testing; -import com.android.tools.build.apkzlib.zip.CentralDirectoryHeader; -import com.android.tools.build.apkzlib.zip.CompressionMethod; -import com.android.tools.build.apkzlib.zip.StoredEntry; -import com.android.tools.build.apkzlib.zip.ZFile; +import com.android.zipflinger.Entry; +import com.android.zipflinger.ZipMap; import java.io.File; import java.io.IOException; import java.util.regex.Pattern; @@ -39,29 +37,21 @@ public final class ZipAlignCheck { * @return {@code true} if the file has been correctly zipaligned. */ public static boolean checkAlignment(File zip) throws IOException { - try (ZFile zfile = new ZFile(zip)) { - for (StoredEntry entry : zfile.entries()) { - CentralDirectoryHeader header = entry.getCentralDirectoryHeader(); - - // Alignment only applies to uncompressed files. - if (header.getCompressionInfoWithWait().getMethod().equals(CompressionMethod.STORE)) { - int expectedAligment = - NATIVE_LIB_PATTERN.matcher(header.getName()).matches() - ? PAGE_ALIGNMENT - : FOUR_BYTE_ALIGNMENT; - - long dataOffset = header.getOffset() + entry.getLocalHeaderSize(); - if (dataOffset % expectedAligment != 0) { - System.out.println( - String.format( - "File '%s' is not aligned. dataOffset=%d, expectedAlignment=%d", - header.getName(), dataOffset, expectedAligment)); - return false; - } + ZipMap zipMap = ZipMap.from(zip.toPath()); + for (Entry entry : zipMap.getEntries().values()) { + int expectedAligment = + NATIVE_LIB_PATTERN.matcher(entry.getName()).matches() + ? PAGE_ALIGNMENT + : FOUR_BYTE_ALIGNMENT; + if (!entry.isCompressed()) { + if (entry.getPayloadLocation().first % expectedAligment != 0) { + System.out.printf( + "File '%s' is not aligned. dataOffset=%d, expectedAlignment=%d%n", + entry.getName(), entry.getPayloadLocation().first, expectedAligment); + return false; } } } - return true; } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java index 6d45dad1..f4b77c52 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java @@ -42,7 +42,7 @@ public void noTiers_ok() { BundleModule moduleB = new BundleModuleBuilder("b").setManifest(androidManifest("com.test.app")).build(); - new AbiParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); + new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); } @Test @@ -60,7 +60,21 @@ public void sameTiers_ok() { .setManifest(androidManifest("com.test.app")) .build(); - new AbiParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); + new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void multipleFilesPerTieredDirectory_ok() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img#tier_0/imageA.jpg") + .addFile("assets/img#tier_0/imageB.jpg") + .addFile("assets/img#tier_1/image1.jpg") + .addFile("assets/img#tier_1/image2.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA)); } @Test @@ -80,15 +94,14 @@ public void sameTiersAndNoTier_ok() { BundleModule moduleC = new BundleModuleBuilder("c").setManifest(androidManifest("com.test.app")).build(); - new AbiParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB, moduleC)); + new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB, moduleC)); } @Test - public void differentTiers_throws() { + public void differentTiersInDifferentModules_throws() { BundleModule moduleA = new BundleModuleBuilder("a") .addFile("assets/img#tier_0/image.jpg") - .addFile("assets/img#tier_2/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); BundleModule moduleB = @@ -109,6 +122,70 @@ public void differentTiers_throws() { .hasMessageThat() .contains( "All modules with device tier targeting must support the same set of tiers, but module" - + " 'a' supports [0, 2] and module 'b' supports [0, 1]."); + + " 'a' supports [0] and module 'b' supports [0, 1]."); + } + + @Test + public void differentTiersInDifferentFolders_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#tier_0/image.jpg") + .addFile("assets/img1#tier_1/image.jpg") + .addFile("assets/img2#tier_0/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "All device-tier-targeted folders in a module must support the same set of tiers, but" + + " module 'a' supports [0, 1] and folder 'assets/img2' supports only [0]."); + } + + @Test + public void tierNumbersNotStartingFromZero_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#tier_1/image.jpg") + .addFile("assets/img1#tier_2/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "All modules with device tier targeting must support the same contiguous" + + " range of tier values starting from 0, but module 'a' supports [1, 2]."); + } + + @Test + public void tierNumbersNotContiguous_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#tier_0/image.jpg") + .addFile("assets/img1#tier_2/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "All modules with device tier targeting must support the same contiguous" + + " range of tier values starting from 0, but module 'a' supports [0, 2]."); } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java index af1fd2e7..fe8750b6 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import java.nio.file.Path; @@ -55,7 +56,9 @@ public void moduleZipFile_withoutAndroidManifest_throws() throws Exception { InvalidBundleException exception = assertThrows( InvalidBundleException.class, - () -> new MandatoryFilesPresenceValidator().validateModuleZipFile(moduleZip)); + () -> + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES) + .validateModuleZipFile(moduleZip)); assertThat(exception) .hasMessageThat() @@ -71,7 +74,8 @@ public void moduleZipFile_withAllMandatoryFiles_ok() throws Exception { .writeTo(tempFolder.resolve("base.zip")); try (ZipFile moduleZip = new ZipFile(modulePath.toFile())) { - new MandatoryFilesPresenceValidator().validateModuleZipFile(moduleZip); + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES) + .validateModuleZipFile(moduleZip); } } @@ -86,7 +90,9 @@ public void bundleZipFile_withoutBundleConfig_throws() throws Exception { InvalidBundleException exception = assertThrows( InvalidBundleException.class, - () -> new MandatoryFilesPresenceValidator().validateBundleZipFile(bundleZip)); + () -> + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES) + .validateBundleZipFile(bundleZip)); assertThat(exception).hasMessageThat().contains("missing required file 'BundleConfig.pb'"); } @@ -104,7 +110,9 @@ public void bundleZipFile_withoutAndroidManifestInModule_throws() throws Excepti InvalidBundleException exception = assertThrows( InvalidBundleException.class, - () -> new MandatoryFilesPresenceValidator().validateBundleZipFile(bundleZip)); + () -> + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES) + .validateBundleZipFile(bundleZip)); assertThat(exception) .hasMessageThat() @@ -121,7 +129,8 @@ public void bundleZipFile_withAllMandatoryFiles_ok() throws Exception { .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { - new MandatoryFilesPresenceValidator().validateBundleZipFile(bundleZip); + new MandatoryFilesPresenceValidator(AppBundle.NON_MODULE_DIRECTORIES) + .validateBundleZipFile(bundleZip); } } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java new file mode 100644 index 00000000..0e375aef --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_LOCATION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_TREE_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallLocation; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMainActivity; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermission; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermissionGroup; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermissionTree; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkLibraryElement; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSharedUserId; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitNameService; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import org.junit.Test; +import org.junit.experimental.theories.Theories; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class SdkAndroidManifestValidatorTest { + + private static final String BASE_MODULE_NAME = "base"; + private static final String PKG_NAME = "com.test.app"; + + @Test + public void manifest_withoutSdkLibrary_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, + withMinSdkVersion(32), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "5"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains("There must be exactly one <" + SDK_LIBRARY_ELEMENT_NAME + "> element."); + } + + @Test + public void manifest_majorVersionIsNotLong_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, + withMinSdkVersion(32), + withSdkLibraryElement("Foo16"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "5"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "SDK Major version in <" + SDK_LIBRARY_ELEMENT_NAME + "> cannot be parsed to a Long."); + } + + @Test + public void manifest_patchVersionIsNotLong_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, + withMinSdkVersion(32), + withSdkLibraryElement("99"), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "20bar"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "SDK Patch version in <" + META_DATA_ELEMENT_NAME + "> cannot be parsed to a Long."); + } + + @Test + public void manifest_withPreferExternal_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, withSdkLibraryElement("99"), withInstallLocation("preferExternal"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "'" + + INSTALL_LOCATION_ATTRIBUTE_NAME + + "' in must be 'internalOnly' for SDK bundles if it is set."); + } + + @Test + public void manifest_withPermission_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest(androidManifest(PKG_NAME, withSdkLibraryElement("99"), withPermission())) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "<" + + PERMISSION_ELEMENT_NAME + + "> cannot be declared in the manifest of an SDK bundle."); + } + + @Test + public void manifest_withPermissionGroup_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest(PKG_NAME, withSdkLibraryElement("21321"), withPermissionGroup())) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "<" + + PERMISSION_GROUP_ELEMENT_NAME + + "> cannot be declared in the manifest of an SDK bundle."); + } + + @Test + public void manifest_withPermissionTree_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest(PKG_NAME, withSdkLibraryElement("6456"), withPermissionTree())) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "<" + + PERMISSION_TREE_ELEMENT_NAME + + "> cannot be declared in the manifest of an SDK bundle."); + } + + @Test + public void manifest_withSharedUserId_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, withSdkLibraryElement("99"), withSharedUserId("sharedUserId"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "'" + + SHARED_USER_ID_ATTRIBUTE_NAME + + "' attribute cannot be used in the manifest of an SDK bundle."); + } + + @Test + public void manifest_withActivityComponent_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, withSdkLibraryElement("99"), withMainActivity("myFunActivity"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "None of , , , or can be declared in the" + + " manifest of an SDK bundle."); + } + + @Test + public void manifest_withServiceComponent_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, + withSdkLibraryElement("1"), + withSplitNameService("serviceName", "splitName"))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains( + "None of , , , or can be declared in the" + + " manifest of an SDK bundle."); + } + + @Test + public void manifest_withSplitId_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, withSdkLibraryElement("432094390"), withSplitId(BASE_MODULE_NAME))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains("'split' attribute cannot be used in the manifest of an SDK bundle."); + } + + @Test + public void manifest_withoutPatchVersionSet_ok() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest(PKG_NAME, withMinSdkVersion(32), withSdkLibraryElement("16"))) + .build(); + + new SdkAndroidManifestValidator().validateModule(module); + } + + @Test + public void manifest_valid_ok() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest( + androidManifest( + PKG_NAME, + withMinSdkVersion(30), + withMetadataValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME, "100"), + withSdkLibraryElement("1"))) + .build(); + + new SdkAndroidManifestValidator().validateModule(module); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java new file mode 100644 index 00000000..d64c35c8 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import java.nio.file.Path; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SdkBundleHasOneModuleValidatorTest { + + private static final byte[] DUMMY_CONTENT = new byte[1]; + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + private Path tempFolder; + + @Before + public void setUp() { + tempFolder = tmp.getRoot().toPath(); + } + + @Test + public void sdkBundleZipFile_multipleModules_throws() throws Exception { + Path bundlePath = + new ZipBuilder() + .addFileWithContent(ZipPath.create("BundleConfig.pb"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/manifest/AndroidManifest.xml"), DUMMY_CONTENT) + .addFileWithContent( + ZipPath.create("feature/manifest/AndroidManifest.xml"), DUMMY_CONTENT) + .writeTo(tempFolder.resolve("bundle.asb")); + + try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkBundleHasOneModuleValidator().validateBundleZipFile(bundleZip)); + + assertThat(exception).hasMessageThat().contains("SDK bundles need exactly one module"); + } + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidatorTest.java new file mode 100644 index 00000000..d6150c4f --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleModuleNameValidatorTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + */ + +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.model.BundleModuleName.BASE_MODULE_NAME; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SdkBundleModuleNameValidatorTest { + + @Test + public void nonBaseModuleName_throws() { + BundleModule base = + BundleModule.builder() + .setName(BundleModuleName.create("invalidModuleName")) + .setAndroidManifestProto(androidManifest("com.foo.bar")) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + + InvalidBundleException expected = + assertThrows( + InvalidBundleException.class, + () -> new SdkBundleModuleNameValidator().validateModule(base)); + + assertThat(expected) + .hasMessageThat() + .contains("The SDK bundle module must be named '" + BASE_MODULE_NAME + "'"); + } + + @Test + public void baseModuleName_ok() { + BundleModule base = + BundleModule.builder() + .setName(BASE_MODULE_NAME) + .setAndroidManifestProto(androidManifest("com.foo.bar")) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + + new SdkBundleModuleNameValidator().validateModule(base); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java index 37c7b29e..0197e42e 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java @@ -93,6 +93,29 @@ public void validateBundleZipFile_invokesRightSubValidatorMethods() throws Excep } } + @Test + public void validateSdkBundleZipFile_invokesRightSubValidatorMethods() throws Exception { + Path bundlePath = + new ZipBuilder() + .addDirectory(ZipPath.create("directory")) + .addFileWithContent(ZipPath.create("file.txt"), DUMMY_CONTENT) + .writeTo(tempFolder.resolve("bundle.asb")); + + try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { + new ValidatorRunner(ImmutableList.of(validator)).validateBundleZipFile(bundleZip); + + ArgumentCaptor zipEntryArgs = ArgumentCaptor.forClass(ZipEntry.class); + + verify(validator).validateBundleZipFile(eq(bundleZip)); + verify(validator, atLeastOnce()) + .validateBundleZipEntry(eq(bundleZip), zipEntryArgs.capture()); + verifyNoMoreInteractions(validator); + + assertThat(zipEntryArgs.getAllValues().stream().map(ZipEntry::getName)) + .containsExactly("directory/", "file.txt"); + } + } + @Test public void validateBundle_invokesRightSubValidatorMethods() throws Exception { Path bundlePath = diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorsTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorsTest.java index 4e4c6767..7e263a44 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorsTest.java @@ -59,6 +59,8 @@ public void eachSubValidatorIsRegistered() throws Exception { .addAll(toClasses(AppBundleValidator.DEFAULT_BUNDLE_SUB_VALIDATORS)) .addAll(toClasses(BundleModulesValidator.MODULE_FILE_SUB_VALIDATORS)) .addAll(toClasses(BundleModulesValidator.MODULES_SUB_VALIDATORS)) + .addAll(toClasses(SdkBundleValidator.DEFAULT_BUNDLE_FILE_SUB_VALIDATORS)) + .addAll(toClasses(SdkBundleValidator.DEFAULT_BUNDLE_SUB_VALIDATORS)) .build(); assertThat(existingSubValidators).containsExactlyElementsIn(registeredSubValidators);