From 9831cec4ab432c960d3670caef7c174027ce271f Mon Sep 17 00:00:00 2001 From: Iurii Makhno Date: Thu, 23 Apr 2020 14:34:12 +0100 Subject: [PATCH] Prepare for release 0.14.0. --- README.md | 2 +- gradle.properties | 2 +- .../build/bundletool/BundleToolMain.java | 10 + .../bundletool/commands/BuildApksCommand.java | 29 +- .../bundletool/commands/BuildApksManager.java | 4 +- .../commands/BuildBundleCommand.java | 4 +- .../bundletool/commands/CommandUtils.java | 70 ++ .../commands/GetDeviceSpecCommand.java | 25 +- .../bundletool/commands/GetSizeCommand.java | 30 +- .../commands/InstallApksCommand.java | 26 +- .../commands/InstallMultiApksCommand.java | 465 ++++++++++ .../device/AbstractSizeAggregator.java | 275 ++++++ .../device/AdbCommandOutputValidator.java | 35 + .../build/bundletool/device/ApkMatcher.java | 15 +- .../device/AssetModuleSizeAggregator.java | 105 +++ .../device/BadgingPackageNameParser.java | 48 + .../build/bundletool/device/DdmlibDevice.java | 12 + .../tools/build/bundletool/device/Device.java | 7 + .../bundletool/device/DeviceAnalyzer.java | 13 +- .../device/MultiPackagesInstaller.java | 186 ++++ .../bundletool/device/PackagesParser.java | 38 + .../bundletool/device/SdkVersionMatcher.java | 21 + .../bundletool/device/SessionIdParser.java | 53 ++ .../bundletool/device/VariantMatcher.java | 4 +- .../device/VariantTotalSizeAggregator.java | 229 +---- .../bundletool/io/ApkSerializerHelper.java | 64 +- .../bundletool/io/ApkSerializerManager.java | 3 +- .../io/StandaloneApkSerializer.java | 3 +- .../tools/build/bundletool/io/ZipBuilder.java | 7 +- .../mergers/ModuleSplitsToShardMerger.java | 5 +- .../mergers/SameTargetingMerger.java | 11 + .../build/bundletool/model/Aapt2Command.java | 31 + .../bundletool/model/InputStreamSupplier.java | 51 +- .../model/InputStreamSuppliers.java | 3 +- .../build/bundletool/model/ModuleEntry.java | 16 +- .../build/bundletool/model/ModuleSplit.java | 15 +- .../model/targeting/TargetingGenerator.java | 7 +- .../bundletool/model/utils/ApkSizeUtils.java | 9 +- .../model/utils/ConfigurationSizesMerger.java | 94 ++ .../model/utils/SdkToolsLocator.java | 3 +- .../bundletool/model/utils/ZipUtils.java | 32 - .../model/utils/files/BufferedIo.java | 4 +- .../model/version/BundleToolVersion.java | 2 +- .../model/version/VersionGuardedFeature.java | 13 +- .../EmbeddedApkSigningPreprocessor.java | 96 ++ .../size/ApkBreakdownGenerator.java | 59 +- .../bundletool/splitters/BundleSharder.java | 3 +- .../bundletool/splitters/ModuleSplitter.java | 4 +- .../ScreenDensityResourcesSplitter.java | 20 +- .../validation/BundleFilesValidator.java | 3 +- src/main/proto/config.proto | 8 + src/main/proto/devices.proto | 4 + src/main/proto/files.proto | 6 +- .../commands/BuildApksManagerTest.java | 145 ++- .../commands/GetSizeCommandTest.java | 244 ++++- .../commands/InstallMultiApksCommandTest.java | 844 ++++++++++++++++++ .../device/AssetModuleSizeAggregatorTest.java | 196 ++++ .../device/BadgingPackageNameParserTest.java | 84 ++ .../bundletool/device/PackagesParserTest.java | 43 + .../device/SdkVersionMatcherTest.java | 26 + .../device/SessionIdParserTest.java | 65 ++ .../VariantTotalSizeAggregatorTest.java | 1 - .../bundletool/model/BundleMetadataTest.java | 3 +- .../model/InputStreamSuppliersTest.java | 5 +- .../bundletool/model/ModuleEntryTest.java | 25 +- .../targeting/TargetingGeneratorTest.java | 16 - .../model/utils/ApkSizeUtilsTest.java | 12 +- .../utils/ConfigurationSizesMergerTest.java | 190 ++++ .../model/utils/SdkToolsLocatorTest.java | 27 +- .../size/ApkBreakdownGeneratorTest.java | 124 ++- .../ScreenDensityResourcesSplitterTest.java | 135 ++- .../testing/ApksArchiveHelpers.java | 15 +- .../testing/BundleConfigBuilder.java | 6 + .../bundletool/testing/DeviceFactory.java | 21 +- .../build/bundletool/testing/FakeDevice.java | 15 +- .../validation/BundleFilesValidatorTest.java | 9 +- 76 files changed, 4036 insertions(+), 499 deletions(-) create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/CommandUtils.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/AdbCommandOutputValidator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/BadgingPackageNameParser.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/MultiPackagesInstaller.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/PackagesParser.java create mode 100644 src/main/java/com/android/tools/build/bundletool/device/SessionIdParser.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java create mode 100644 src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/device/BadgingPackageNameParserTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/device/PackagesParserTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/device/SessionIdParserTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java diff --git a/README.md b/README.md index 58dffb4b..71028d24 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [0.13.4](https://github.com/google/bundletool/releases) +Latest release: [0.14.0](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 0d313604..e6822cbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 0.13.4 +release_version = 0.14.0 diff --git a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java index 550756d7..21523ddb 100644 --- a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java +++ b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java @@ -23,6 +23,7 @@ import com.android.tools.build.bundletool.commands.GetDeviceSpecCommand; import com.android.tools.build.bundletool.commands.GetSizeCommand; import com.android.tools.build.bundletool.commands.InstallApksCommand; +import com.android.tools.build.bundletool.commands.InstallMultiApksCommand; import com.android.tools.build.bundletool.commands.ValidateBundleCommand; import com.android.tools.build.bundletool.commands.VersionCommand; import com.android.tools.build.bundletool.device.AdbServer; @@ -88,6 +89,11 @@ static void main(String[] args, Runtime runtime) { InstallApksCommand.fromFlags(flags, adbServer).execute(); } break; + case InstallMultiApksCommand.COMMAND_NAME: + try (AdbServer adbServer = DdmlibAdbServer.getInstance()) { + InstallMultiApksCommand.fromFlags(flags, adbServer).execute(); + } + break; case ValidateBundleCommand.COMMAND_NAME: ValidateBundleCommand.fromFlags(flags).execute(); break; @@ -134,6 +140,7 @@ public static void help() { ExtractApksCommand.help(), GetDeviceSpecCommand.help(), InstallApksCommand.help(), + InstallMultiApksCommand.help(), ValidateBundleCommand.help(), DumpCommand.help(), GetSizeCommand.help(), @@ -165,6 +172,9 @@ public static void help(String commandName, Runtime runtime) { case InstallApksCommand.COMMAND_NAME: commandHelp = InstallApksCommand.help(); break; + case InstallMultiApksCommand.COMMAND_NAME: + commandHelp = InstallMultiApksCommand.help(); + break; case ValidateBundleCommand.COMMAND_NAME: commandHelp = ValidateBundleCommand.help(); break; 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 1b3a99a3..61d8449b 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 @@ -20,6 +20,7 @@ import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM_COMPRESSED; import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; +import static com.android.tools.build.bundletool.commands.CommandUtils.ANDROID_SERIAL_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.ANDROID_HOME_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.SYSTEM_PATH_VARIABLE; import static com.google.common.base.Preconditions.checkArgument; @@ -42,7 +43,6 @@ import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; -import com.android.tools.build.bundletool.model.utils.SdkToolsLocator; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.splitters.DexCompressionSplitter; import com.android.tools.build.bundletool.splitters.NativeLibrariesCompressionSplitter; @@ -113,7 +113,6 @@ public final boolean isAnySystemMode() { private static final Flag ADB_PATH_FLAG = Flag.path("adb"); private static final Flag CONNECTED_DEVICE_FLAG = Flag.booleanFlag("connected-device"); private static final Flag DEVICE_ID_FLAG = Flag.string("device-id"); - private static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; private static final Flag> MODULES_FLAG = Flag.stringSet("modules"); private static final Flag DEVICE_SPEC_FLAG = Flag.path("device-spec"); @@ -492,19 +491,8 @@ static BuildApksCommand fromFlags( // Applied only when --connected-device flag is set, because we don't want to fail command // if ADB cannot be found in a normal mode. - Optional adbPathFromFlag = ADB_PATH_FLAG.getValue(flags); if (connectedDeviceMode) { - Path adbPath = - adbPathFromFlag.orElseGet( - () -> - new SdkToolsLocator() - .locateAdb(systemEnvironmentProvider) - .orElseThrow( - () -> - new CommandExecutionException( - "Unable to determine the location of ADB. Please set the --adb " - + "flag or define ANDROID_HOME or PATH environment " - + "variable."))); + Path adbPath = CommandUtils.getAdbPath(flags, ADB_PATH_FLAG, systemEnvironmentProvider); buildApksCommand.setAdbPath(adbPath).setAdbServer(adbServer); } @@ -531,22 +519,11 @@ static BuildApksCommand fromFlags( public Path execute() { try (TempDirectory tempDir = new TempDirectory()) { Aapt2Command aapt2 = - getAapt2Command().orElseGet(() -> extractAapt2FromJar(tempDir.getPath())); + getAapt2Command().orElseGet(() -> CommandUtils.extractAapt2FromJar(tempDir.getPath())); return new BuildApksManager(this, aapt2, tempDir.getPath()).execute(); } } - private static Aapt2Command extractAapt2FromJar(Path tempDir) { - return new SdkToolsLocator() - .extractAapt2(tempDir) - .map(Aapt2Command::createFromExecutablePath) - .orElseThrow( - () -> - new CommandExecutionException( - "Could not extract aapt2: consider updating bundletool to a more recent " - + "version or providing the path to aapt2 using the flag --aapt2.")); - } - /** * Creates an internal executor service that uses at most the given number of threads. * 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 068126a3..89207704 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 @@ -64,6 +64,7 @@ import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.optimizations.OptimizationsMerger; import com.android.tools.build.bundletool.preprocessors.AppBundle64BitNativeLibrariesPreprocessor; +import com.android.tools.build.bundletool.preprocessors.EmbeddedApkSigningPreprocessor; import com.android.tools.build.bundletool.preprocessors.EntryCompressionPreprocessor; import com.android.tools.build.bundletool.preprocessors.LocalTestingPreprocessor; import com.android.tools.build.bundletool.splitters.ApkGenerationConfiguration; @@ -184,7 +185,7 @@ private void executeWithZip( // Note: Universal APK is a special type of standalone, with no optimization dimensions. ImmutableList modulesToFuse = requestedModules.isEmpty() - ? modulesToFuse(appBundle.getFeatureModules().values().asList()) + ? modulesToFuse(getModulesForStandaloneApks(appBundle)) : requestedModules.asList(); generatedApksBuilder.setStandaloneApks( new ShardedApksGenerator( @@ -521,6 +522,7 @@ private AppBundle applyPreprocessors(AppBundle bundle) { bundle = new LocalTestingPreprocessor().preprocess(bundle); } bundle = new EntryCompressionPreprocessor().preprocess(bundle); + bundle = new EmbeddedApkSigningPreprocessor().preprocess(bundle); return bundle; } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildBundleCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildBundleCommand.java index abc5666b..2435254b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildBundleCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildBundleCommand.java @@ -316,9 +316,7 @@ private static Optional generateApexImagesTargeting(BundleModule mod } return Optional.of( - new TargetingGenerator() - .generateTargetingForApexImages( - module.getBundleConfig().getApexConfig(), apexImageFiles, hasBuildInfo)); + new TargetingGenerator().generateTargetingForApexImages(apexImageFiles, hasBuildInfo)); } private static BundleConfig parseBundleConfigJson(Path bundleConfigJsonPath) { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/CommandUtils.java b/src/main/java/com/android/tools/build/bundletool/commands/CommandUtils.java new file mode 100644 index 00000000..c01603ec --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/CommandUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 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.flags.Flag; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.model.Aapt2Command; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.utils.SdkToolsLocator; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import java.nio.file.Path; +import java.util.Optional; + +final class CommandUtils { + static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; + + private CommandUtils() {} + + static Path getAdbPath( + ParsedFlags flags, Flag adbFlag, SystemEnvironmentProvider systemEnvironmentProvider) { + return adbFlag + .getValue(flags) + .orElseGet( + () -> + new SdkToolsLocator() + .locateAdb(systemEnvironmentProvider) + .orElseThrow( + () -> + new CommandExecutionException( + "Unable to determine the location of ADB. Please set the --adb " + + "flag or define ANDROID_HOME or PATH environment " + + "variable."))); + } + + static Optional getDeviceSerialName( + ParsedFlags flags, + Flag deviceIdFlag, + SystemEnvironmentProvider systemEnvironmentProvider) { + Optional deviceSerialName = deviceIdFlag.getValue(flags); + if (!deviceSerialName.isPresent()) { + deviceSerialName = systemEnvironmentProvider.getVariable(ANDROID_SERIAL_VARIABLE); + } + return deviceSerialName; + } + + static Aapt2Command extractAapt2FromJar(Path tempDir) { + return new SdkToolsLocator() + .extractAapt2(tempDir) + .map(Aapt2Command::createFromExecutablePath) + .orElseThrow( + () -> + new CommandExecutionException( + "Could not extract aapt2: consider updating bundletool to a more recent " + + "version or providing the path to aapt2 using the flag --aapt2.")); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java index ce2b7769..b6b1108b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.CommandUtils.ANDROID_SERIAL_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.ANDROID_HOME_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.SYSTEM_PATH_VARIABLE; import static java.nio.charset.StandardCharsets.UTF_8; @@ -27,10 +28,8 @@ import com.android.tools.build.bundletool.device.DeviceAnalyzer; import com.android.tools.build.bundletool.flags.Flag; import com.android.tools.build.bundletool.flags.ParsedFlags; -import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; -import com.android.tools.build.bundletool.model.utils.SdkToolsLocator; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; import com.google.auto.value.AutoValue; @@ -56,8 +55,6 @@ public abstract class GetDeviceSpecCommand { private static final Flag OUTPUT_FLAG = Flag.path("output"); private static final Flag OVERWRITE_OUTPUT_FLAG = Flag.booleanFlag("overwrite"); - private static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; - private static final SystemEnvironmentProvider DEFAULT_PROVIDER = new DefaultSystemEnvironmentProvider(); @@ -123,25 +120,11 @@ public static GetDeviceSpecCommand fromFlags( GetDeviceSpecCommand.Builder builder = builder().setAdbServer(adbServer).setOutputPath(OUTPUT_FLAG.getRequiredValue(flags)); - Optional deviceSerialName = DEVICE_ID_FLAG.getValue(flags); - if (!deviceSerialName.isPresent()) { - deviceSerialName = systemEnvironmentProvider.getVariable(ANDROID_SERIAL_VARIABLE); - } + Optional deviceSerialName = + CommandUtils.getDeviceSerialName(flags, DEVICE_ID_FLAG, systemEnvironmentProvider); deviceSerialName.ifPresent(builder::setDeviceId); - Path adbPath = - ADB_PATH_FLAG - .getValue(flags) - .orElseGet( - () -> - new SdkToolsLocator() - .locateAdb(systemEnvironmentProvider) - .orElseThrow( - () -> - new CommandExecutionException( - "Unable to determine the location of ADB. Please set the --adb " - + "flag or define ANDROID_HOME or PATH environment " - + "variable."))); + Path adbPath = CommandUtils.getAdbPath(flags, ADB_PATH_FLAG, systemEnvironmentProvider); builder.setAdbPath(adbPath); OVERWRITE_OUTPUT_FLAG.getValue(flags).ifPresent(builder::setOverwriteOutput); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java index 80ca2342..18cde5fd 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java @@ -19,17 +19,21 @@ import static com.android.tools.build.bundletool.commands.GetSizeCommand.GetSizeSubcommand.STRING_TO_SUBCOMMAND; import static com.android.tools.build.bundletool.commands.GetSizeCommand.GetSizeSubcommand.TOTAL; import static com.android.tools.build.bundletool.model.utils.ApkSizeUtils.getCompressedSizeByApkPaths; +import static com.android.tools.build.bundletool.model.utils.ApkSizeUtils.getVariantCompressedSizeByApkPaths; import static com.android.tools.build.bundletool.model.utils.CollectorUtils.combineMaps; import static com.android.tools.build.bundletool.model.utils.GetSizeCsvUtils.getSizeTotalOutputInCsv; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; +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.Commands.BuildApksResult; import com.android.bundle.Commands.Variant; import com.android.bundle.Devices.DeviceSpec; 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.AssetModuleSizeAggregator; import com.android.tools.build.bundletool.device.DeviceSpecParser; import com.android.tools.build.bundletool.device.VariantMatcher; import com.android.tools.build.bundletool.device.VariantTotalSizeAggregator; @@ -40,6 +44,7 @@ import com.android.tools.build.bundletool.model.GetSizeRequest.Dimension; import com.android.tools.build.bundletool.model.SizeConfiguration; import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.utils.ConfigurationSizesMerger; import com.android.tools.build.bundletool.model.utils.ResultUtils; import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; import com.android.tools.build.bundletool.model.version.Version; @@ -220,20 +225,37 @@ ConfigurationSizes getSizeTotalInternal() { ImmutableList variants = new VariantMatcher(getDeviceSpec(), getInstant()).getAllMatchingVariants(buildApksResult); - ImmutableMap compressedSizeByApkPaths = - getCompressedSizeByApkPaths(variants, getApksArchivePath()); + ImmutableMap variantCompressedSizeByApkPaths = + getVariantCompressedSizeByApkPaths(variants, getApksArchivePath()); + + ImmutableList assetModuleApks = + buildApksResult.getAssetSliceSetList().stream() + .flatMap(module -> module.getApkDescriptionList().stream()) + .map(ApkDescription::getPath) + .collect(toImmutableList()); + ImmutableMap assetModuleCompressedSizeByApkPaths = + getCompressedSizeByApkPaths(assetModuleApks, getApksArchivePath()); ImmutableMap minSizeConfigurationMap = ImmutableMap.of(); ImmutableMap maxSizeConfigurationMap = ImmutableMap.of(); for (Variant variant : variants) { - ConfigurationSizes configurationSizes = + ConfigurationSizes variantConfigurationSizes = new VariantTotalSizeAggregator( - compressedSizeByApkPaths, + variantCompressedSizeByApkPaths, Version.of(buildApksResult.getBundletool().getVersion()), variant, this) .getSize(); + ConfigurationSizes assetModuleConfigurationSizes = + new AssetModuleSizeAggregator( + buildApksResult.getAssetSliceSetList(), + variant.getTargeting(), + assetModuleCompressedSizeByApkPaths, + this) + .getSize(); + ConfigurationSizes configurationSizes = + ConfigurationSizesMerger.merge(variantConfigurationSizes, assetModuleConfigurationSizes); minSizeConfigurationMap = combineMaps( minSizeConfigurationMap, configurationSizes.getMinSizeConfigurationMap(), Math::min); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java index 7c1c7e8e..78337468 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.CommandUtils.ANDROID_SERIAL_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.ANDROID_HOME_VARIABLE; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.SYSTEM_PATH_VARIABLE; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkDirectoryExists; @@ -42,7 +43,6 @@ import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.ResultUtils; -import com.android.tools.build.bundletool.model.utils.SdkToolsLocator; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; @@ -65,8 +65,6 @@ public abstract class InstallApksCommand { private static final Flag ALLOW_DOWNGRADE_FLAG = Flag.booleanFlag("allow-downgrade"); private static final Flag ALLOW_TEST_ONLY_FLAG = Flag.booleanFlag("allow-test-only"); - private static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; - private static final SystemEnvironmentProvider DEFAULT_PROVIDER = new DefaultSystemEnvironmentProvider(); @@ -118,24 +116,10 @@ public static InstallApksCommand fromFlags(ParsedFlags flags, AdbServer adbServe public static InstallApksCommand fromFlags( ParsedFlags flags, SystemEnvironmentProvider systemEnvironmentProvider, AdbServer adbServer) { Path apksArchivePath = APKS_ARCHIVE_FILE_FLAG.getRequiredValue(flags); - Path adbPath = - ADB_PATH_FLAG - .getValue(flags) - .orElseGet( - () -> - new SdkToolsLocator() - .locateAdb(systemEnvironmentProvider) - .orElseThrow( - () -> - new CommandExecutionException( - "Unable to determine the location of ADB. Please set the --adb " - + "flag or define ANDROID_HOME or PATH environment " - + "variable."))); - - Optional deviceSerialName = DEVICE_ID_FLAG.getValue(flags); - if (!deviceSerialName.isPresent()) { - deviceSerialName = systemEnvironmentProvider.getVariable(ANDROID_SERIAL_VARIABLE); - } + Path adbPath = CommandUtils.getAdbPath(flags, ADB_PATH_FLAG, systemEnvironmentProvider); + + Optional deviceSerialName = + CommandUtils.getDeviceSerialName(flags, DEVICE_ID_FLAG, systemEnvironmentProvider); Optional> modules = MODULES_FLAG.getValue(flags); Optional allowDowngrade = ALLOW_DOWNGRADE_FLAG.getValue(flags); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java new file mode 100644 index 00000000..ea831c9f --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2020 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.CommandUtils.ANDROID_SERIAL_VARIABLE; +import static com.android.tools.build.bundletool.model.utils.ResultUtils.readTableOfContents; +import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.ANDROID_HOME_VARIABLE; +import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.SYSTEM_PATH_VARIABLE; +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.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap; +import static com.google.common.collect.Streams.stream; +import static java.util.function.Function.identity; + +import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Devices.DeviceSpec; +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; +import com.android.tools.build.bundletool.device.AdbShellCommandTask; +import com.android.tools.build.bundletool.device.BadgingPackageNameParser; +import com.android.tools.build.bundletool.device.Device; +import com.android.tools.build.bundletool.device.DeviceAnalyzer; +import com.android.tools.build.bundletool.device.IncompatibleDeviceException; +import com.android.tools.build.bundletool.device.MultiPackagesInstaller; +import com.android.tools.build.bundletool.device.MultiPackagesInstaller.InstallableApk; +import com.android.tools.build.bundletool.device.PackagesParser; +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.model.Aapt2Command; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +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.BufferedIo; +import com.google.auto.value.AutoValue; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Installs multiple APKs on a connected device atomically. + * + *

If any APKs fails to install, the entire installation fails. + */ +@AutoValue +public abstract class InstallMultiApksCommand { + private static final Logger logger = Logger.getLogger(InstallMultiApksCommand.class.getName()); + + public static final String COMMAND_NAME = "install-multi-apks"; + + private static final Flag ADB_PATH_FLAG = Flag.path("adb"); + private static final Flag> APKS_ARCHIVES_FLAG = Flag.pathList("apks"); + private static final Flag APKS_ARCHIVE_ZIP_FLAG = Flag.path("apks-zip"); + private static final Flag DEVICE_ID_FLAG = Flag.string("device-id"); + private static final Flag ENABLE_ROLLBACK_FLAG = Flag.booleanFlag("enable-rollback"); + private static final Flag UPDATE_ONLY_FLAG = Flag.booleanFlag("update-only"); + private static final Flag NO_COMMIT_FLAG = Flag.booleanFlag("no-commit"); + private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); + + private static final SystemEnvironmentProvider DEFAULT_PROVIDER = + new DefaultSystemEnvironmentProvider(); + + abstract Path getAdbPath(); + + abstract Optional getAapt2Command(); + + abstract ImmutableList getApksArchivePaths(); + + abstract Optional getApksArchiveZipPath(); + + abstract Optional getDeviceId(); + + abstract boolean getEnableRollback(); + + abstract boolean getUpdateOnly(); + + abstract boolean getNoCommitMode(); + + abstract AdbServer getAdbServer(); + + public static Builder builder() { + return new AutoValue_InstallMultiApksCommand.Builder() + .setEnableRollback(false) + .setNoCommitMode(false) + .setUpdateOnly(false); + } + + /** Builder for the {@link InstallMultiApksCommand}. */ + @AutoValue.Builder + public abstract static class Builder { + abstract Builder setAdbPath(Path adbPath); + + @CanIgnoreReturnValue + abstract Builder setAapt2Command(Aapt2Command value); + + @CanIgnoreReturnValue + abstract Builder setApksArchivePaths(ImmutableList paths); + + abstract ImmutableList.Builder apksArchivePathsBuilder(); + + @CanIgnoreReturnValue + abstract Builder setApksArchiveZipPath(Path value); + + @CanIgnoreReturnValue + Builder addApksArchivePath(Path value) { + apksArchivePathsBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + abstract Builder setDeviceId(String deviceId); + + abstract Builder setEnableRollback(boolean value); + + abstract Builder setUpdateOnly(boolean value); + + abstract Builder setNoCommitMode(boolean value); + + /** The caller is responsible for the lifecycle of the {@link AdbServer}. */ + abstract Builder setAdbServer(AdbServer adbServer); + + public abstract InstallMultiApksCommand build(); + } + + public static InstallMultiApksCommand fromFlags(ParsedFlags flags, AdbServer adbServer) { + return fromFlags(flags, DEFAULT_PROVIDER, adbServer); + } + + public static InstallMultiApksCommand fromFlags( + ParsedFlags flags, SystemEnvironmentProvider systemEnvironmentProvider, AdbServer adbServer) { + Path adbPath = CommandUtils.getAdbPath(flags, ADB_PATH_FLAG, systemEnvironmentProvider); + + InstallMultiApksCommand.Builder command = builder().setAdbPath(adbPath).setAdbServer(adbServer); + CommandUtils.getDeviceSerialName(flags, DEVICE_ID_FLAG, systemEnvironmentProvider) + .ifPresent(command::setDeviceId); + ENABLE_ROLLBACK_FLAG.getValue(flags).ifPresent(command::setEnableRollback); + UPDATE_ONLY_FLAG.getValue(flags).ifPresent(command::setUpdateOnly); + NO_COMMIT_FLAG.getValue(flags).ifPresent(command::setNoCommitMode); + AAPT2_PATH_FLAG + .getValue(flags) + .ifPresent( + aapt2Path -> command.setAapt2Command(Aapt2Command.createFromExecutablePath(aapt2Path))); + + Optional> apksPaths = APKS_ARCHIVES_FLAG.getValue(flags); + Optional apksArchiveZip = APKS_ARCHIVE_ZIP_FLAG.getValue(flags); + if (apksPaths.isPresent() == apksArchiveZip.isPresent()) { + throw new CommandExecutionException("Exactly one of --apks or --apks-zip must be set."); + } + apksPaths.ifPresent(command::setApksArchivePaths); + apksArchiveZip.ifPresent(command::setApksArchiveZipPath); + + flags.checkNoUnknownFlags(); + + return command.build(); + } + + public void execute() throws TimeoutException, IOException { + validateInput(); + + AdbServer adbServer = getAdbServer(); + adbServer.init(getAdbPath()); + + try (TempDirectory tempDirectory = new TempDirectory()) { + DeviceAnalyzer deviceAnalyzer = new DeviceAnalyzer(adbServer); + DeviceSpec deviceSpec = deviceAnalyzer.getDeviceSpec(getDeviceId()); + Device device = deviceAnalyzer.getAndValidateDevice(getDeviceId()); + + MultiPackagesInstaller installer = + new MultiPackagesInstaller(device, getEnableRollback(), getNoCommitMode()); + + Path aapt2Dir = tempDirectory.getPath().resolve("aapt2"); + Files.createDirectory(aapt2Dir); + Supplier aapt2CommandSupplier = + Suppliers.memoize(() -> getOrExtractAapt2Command(aapt2Dir)); + + ImmutableSet existingPackages = + getUpdateOnly() ? listPackagesInstalledOnDevice(device) : ImmutableSet.of(); + + ImmutableListMultimap apkToInstallByPackage = + getActualApksPaths(tempDirectory).stream() + .flatMap( + apksArchivePath -> + stream( + apksWithPackageName(apksArchivePath, deviceSpec, aapt2CommandSupplier))) + .filter(apk -> !getUpdateOnly() || isInstalled(apk, existingPackages)) + .flatMap(apks -> extractApkListFromApks(deviceSpec, apks, tempDirectory).stream()) + .collect(toImmutableListMultimap(InstallableApk::getPackageName, identity())); + + if (apkToInstallByPackage.isEmpty()) { + logger.warning("No packages found to install! Exiting..."); + return; + } + installer.install(apkToInstallByPackage); + } + } + + private static boolean isInstalled(InstallableApk apk, ImmutableSet existingPackages) { + boolean exist = existingPackages.contains(apk.getPackageName()); + if (!exist) { + logger.info( + String.format( + "Package '%s' not present on device, skipping due to --%s.", + apk.getPackageName(), UPDATE_ONLY_FLAG.getName())); + } + return exist; + } + + private static ImmutableSet listPackagesInstalledOnDevice(Device device) { + ImmutableList listPackagesOutput = + new AdbShellCommandTask(device, "pm list packages").execute(); + return new PackagesParser().parse(listPackagesOutput); + } + + private static Optional apksWithPackageName( + Path apkArchivePath, DeviceSpec deviceSpec, Supplier aapt2CommandSupplier) { + BuildApksResult toc = readTableOfContents(apkArchivePath); + if (toc.getPackageName().isEmpty()) { + return getApksWithPackageNameFromAapt2(apkArchivePath, deviceSpec, aapt2CommandSupplier); + } + return Optional.of(InstallableApk.create(apkArchivePath, toc.getPackageName())); + } + + private static Optional getApksWithPackageNameFromAapt2( + Path apksArchivePath, DeviceSpec deviceSpec, Supplier aapt2CommandSupplier) { + try (TempDirectory tempDirectory = new TempDirectory()) { + // Any of the extracted .apk/.apex files will work. + Path extractedFile = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchivePath) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tempDirectory.getPath()) + .build() + .execute() + .get(0); + + String packageName = + BadgingPackageNameParser.parse(aapt2CommandSupplier.get().dumpBadging(extractedFile)); + return Optional.of(InstallableApk.create(apksArchivePath, packageName)); + } catch (IncompatibleDeviceException e) { + logger.warning( + String.format( + "Unable to determine package name of %s, as it is not compatible with the attached" + + " device. Skipping.", + apksArchivePath)); + return Optional.empty(); + } + } + + private void validateInput() { + getApksArchiveZipPath() + .ifPresent( + zip -> { + checkFileExistsAndReadable(zip); + checkFileHasExtension("ZIP file", zip, ".zip"); + }); + getApksArchivePaths().forEach(InstallMultiApksCommand::checkValidApksFile); + checkFileExistsAndExecutable(getAdbPath()); + } + + private static void checkValidApksFile(Path path) { + checkFileExistsAndReadable(path); + checkFileHasExtension("APKS file", path, ".apks"); + } + + /** Extracts the apk/apex files that will be installed from a given .apks. */ + private static ImmutableList extractApkListFromApks( + DeviceSpec deviceSpec, InstallableApk apksArchive, TempDirectory tempDirectory) { + logger.info(String.format("Extracting package '%s'", apksArchive.getPackageName())); + try { + Path output = tempDirectory.getPath().resolve(apksArchive.getPackageName()); + Files.createDirectory(output); + + ExtractApksCommand.Builder extractApksCommand = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchive.getPath()) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(output); + + return extractApksCommand.build().execute().stream() + .map(path -> InstallableApk.create(path, apksArchive.getPackageName())) + .collect(toImmutableList()); + } catch (IncompatibleDeviceException e) { + logger.warning( + String.format( + "Package '%s' is not supported by the attached device (SDK version %d). Skipping.", + apksArchive.getPackageName(), deviceSpec.getSdkVersion())); + return ImmutableList.of(); + } catch (IOException e) { + throw CommandExecutionException.builder() + .withMessage( + String.format( + "Temp directory to extract files for package '%s' can't be created", + apksArchive.getPackageName())) + .withCause(e) + .build(); + } + } + + /** + * Gets the list of actual .apks files to install, extracting them from the .zip file to a temp + * directory if necessary. + */ + private ImmutableList getActualApksPaths(TempDirectory tempDirectory) throws IOException { + return getApksArchiveZipPath().isPresent() + ? extractApksFromZip(getApksArchiveZipPath().get(), tempDirectory) + : getApksArchivePaths(); + } + + /** Extract the .apks files from a zip file containing multiple .apks files. */ + private static ImmutableList extractApksFromZip(Path zipPath, TempDirectory tempDirectory) + throws IOException { + ImmutableList.Builder extractedApks = ImmutableList.builder(); + Path zipExtractedSubDirectory = tempDirectory.getPath().resolve("extracted"); + Files.createDirectory(zipExtractedSubDirectory); + try (ZipFile apksArchiveContainer = new ZipFile(zipPath.toFile())) { + ImmutableList apksToExtractList = + apksArchiveContainer.stream() + .filter( + zipEntry -> + !zipEntry.isDirectory() + && zipEntry.getName().toLowerCase(Locale.ROOT).endsWith(".apks")) + .collect(toImmutableList()); + for (ZipEntry apksToExtract : apksToExtractList) { + Path extractedApksPath = + zipExtractedSubDirectory.resolve( + ZipPath.create(apksToExtract.getName()).getFileName().toString()); + try (InputStream inputStream = BufferedIo.inputStream(apksArchiveContainer, apksToExtract); + OutputStream outputApks = BufferedIo.outputStream(extractedApksPath)) { + ByteStreams.copy(inputStream, outputApks); + extractedApks.add(extractedApksPath); + } + } + } + return extractedApks.build(); + } + + public static CommandHelp help() { + return CommandHelp.builder() + .setCommandName(COMMAND_NAME) + .setCommandDescription( + CommandDescription.builder() + .setShortDescription( + "Atomically install APKs and APEXs from multiple APK Sets to a connected" + + " device.") + .addAdditionalParagraph( + "This will extract and install from the APK Sets only the APKs that would be" + + " served to that device. If the app is not compatible with the device or" + + " if the APK Set was generated for a different type of device," + + " this command will fail. If any one of the APK Sets fails to install," + + " none of the APK Sets will be installed.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(ADB_PATH_FLAG.getName()) + .setExampleValue("path/to/adb") + .setOptional(true) + .setDescription( + "Path to the adb utility. If absent, an attempt will be made to locate it if " + + "the %s or %s environment variable is set.", + ANDROID_HOME_VARIABLE, SYSTEM_PATH_VARIABLE) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(DEVICE_ID_FLAG.getName()) + .setExampleValue("device-serial-name") + .setOptional(true) + .setDescription( + "Device serial name. If absent, this uses the %s environment variable. Either " + + "this flag or the environment variable is required when more than one " + + "device or emulator is connected.", + ANDROID_SERIAL_VARIABLE) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(APKS_ARCHIVES_FLAG.getName()) + .setExampleValue("/path/to/apks1.apks,/path/to/apks2.apks") + .setOptional(true) + .setDescription( + "The list of .apks files to install. Either --apks or --apks-zip" + + " is required.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(APKS_ARCHIVE_ZIP_FLAG.getName()) + .setExampleValue("/path/to/apks_containing.zip") + .setOptional(true) + .setDescription( + "Zip file containing .apks files to install. Either --apks or" + + " --apks-zip is required.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(ENABLE_ROLLBACK_FLAG.getName()) + .setOptional(true) + .setDescription( + "Enables rollback of the entire atomic install by rolling back any one of the" + + " packages.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(UPDATE_ONLY_FLAG.getName()) + .setOptional(true) + .setDescription( + "If set, only packages that are already installed on the device will be" + + " updated. Entirely new packages will not be installed.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(NO_COMMIT_FLAG.getName()) + .setOptional(true) + .setDescription( + "Run the full install commands, but abandon the install session instead of" + + " committing it.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(AAPT2_PATH_FLAG.getName()) + .setExampleValue("path/to/aapt2") + .setOptional(true) + .setDescription("Path to the aapt2 binary to use.") + .build()) + .build(); + } + + /** Utility for providing an Aapt2Command if it is needed, to be used with Suppliers.memoize. */ + private Aapt2Command getOrExtractAapt2Command(Path tempDirectoryForJarCommand) { + if (getAapt2Command().isPresent()) { + return getAapt2Command().get(); + } + return CommandUtils.extractAapt2FromJar(tempDirectoryForJarCommand); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java new file mode 100644 index 00000000..23c8057e --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isAbiMissing; +import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isLocalesMissing; +import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isScreenDensityMissing; +import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isSdkVersionMissing; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ABI; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.LANGUAGE; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SCREEN_DENSITY; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SDK; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.ScreenDensityTargeting; +import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.tools.build.bundletool.commands.GetSizeCommand; +import com.android.tools.build.bundletool.device.DeviceSpecUtils.DeviceSpecFromTargetingBuilder; +import com.android.tools.build.bundletool.model.ConfigurationSizes; +import com.android.tools.build.bundletool.model.GetSizeRequest; +import com.android.tools.build.bundletool.model.GetSizeRequest.Dimension; +import com.android.tools.build.bundletool.model.SizeConfiguration; +import com.android.tools.build.bundletool.model.ZipPath; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.HashMap; +import java.util.Map; + +/** + * Common class to generate {@link ConfigurationSizes} for sets of APKs based on the requested + * dimensions passed to {@link GetSizeCommand}. + * + *

Subclasses should implement the high level logic to get the total size ({@link getSize}) and + * the logic to get the matching apks from a particular combination of targetings ({@link + * getMatchingApks}). + */ +public abstract class AbstractSizeAggregator { + + private static final Joiner COMMA_JOINER = Joiner.on(','); + + protected final ImmutableMap sizeByApkPaths; + protected final GetSizeRequest getSizeRequest; + + protected AbstractSizeAggregator( + ImmutableMap sizeByApkPaths, GetSizeRequest getSizeRequest) { + this.sizeByApkPaths = sizeByApkPaths; + this.getSizeRequest = getSizeRequest; + } + + /** Aggregate the sizes of a set of APKs info a {@link ConfigurationSizes}. */ + public abstract ConfigurationSizes getSize(); + + protected abstract ImmutableList getMatchingApks( + SdkVersionTargeting sdkVersionTargeting, + AbiTargeting abiTargeting, + ScreenDensityTargeting screenDensityTargeting, + LanguageTargeting languageTargeting); + + protected ImmutableSet getAllSdkVersionTargetings( + ImmutableList apkDescriptions) { + ImmutableSet.Builder sdkVersionTargetingOptions = ImmutableSet.builder(); + + if (isSdkVersionMissing(getSizeRequest.getDeviceSpec())) { + sdkVersionTargetingOptions.addAll( + apkDescriptions.stream() + .map(ApkDescription::getTargeting) + .filter(ApkTargeting::hasSdkVersionTargeting) + .map(ApkTargeting::getSdkVersionTargeting) + .collect(toImmutableSet())); + } + + // Adding default targeting (if targetings are empty) to help computing the cartesian product + // across all targetings. + return sdkVersionTargetingOptions.build().isEmpty() + ? ImmutableSet.of(SdkVersionTargeting.getDefaultInstance()) + : sdkVersionTargetingOptions.build(); + } + + protected ImmutableSet getAllAbiTargetings( + ImmutableList apkDescriptions) { + ImmutableSet.Builder abiTargetingOptions = ImmutableSet.builder(); + + if (isAbiMissing(getSizeRequest.getDeviceSpec())) { + abiTargetingOptions.addAll( + apkDescriptions.stream() + .map(ApkDescription::getTargeting) + .filter(ApkTargeting::hasAbiTargeting) + .map(ApkTargeting::getAbiTargeting) + .collect(toImmutableSet())); + } + + // Adding default targeting (if targetings are empty) to help computing the cartesian product + // across all targetings. + return abiTargetingOptions.build().isEmpty() + ? ImmutableSet.of(AbiTargeting.getDefaultInstance()) + : abiTargetingOptions.build(); + } + + protected ImmutableSet getAllScreenDensityTargetings( + ImmutableList apkDescriptions) { + ImmutableSet.Builder screenDensityTargetingOptions = + ImmutableSet.builder(); + + if (isScreenDensityMissing(getSizeRequest.getDeviceSpec())) { + screenDensityTargetingOptions.addAll( + apkDescriptions.stream() + .map(ApkDescription::getTargeting) + .filter(ApkTargeting::hasScreenDensityTargeting) + .map(ApkTargeting::getScreenDensityTargeting) + .collect(toImmutableSet())); + } + + // Adding default targeting (if targetings are empty) to help computing the cartesian product + // across all targetings. + return screenDensityTargetingOptions.build().isEmpty() + ? ImmutableSet.of(ScreenDensityTargeting.getDefaultInstance()) + : screenDensityTargetingOptions.build(); + } + + protected ImmutableSet getAllLanguageTargetings( + ImmutableList apkDescriptions) { + ImmutableSet.Builder languageTargetingOptions = ImmutableSet.builder(); + + if (isLocalesMissing(getSizeRequest.getDeviceSpec())) { + languageTargetingOptions.addAll( + apkDescriptions.stream() + .map(ApkDescription::getTargeting) + .filter(ApkTargeting::hasLanguageTargeting) + .map(ApkTargeting::getLanguageTargeting) + .collect(toImmutableSet())); + } + + // Adding default targeting (if targetings are empty) to help computing the cartesian product + // across all targetings. + return languageTargetingOptions.build().isEmpty() + ? ImmutableSet.of(LanguageTargeting.getDefaultInstance()) + : languageTargetingOptions.build(); + } + + protected ConfigurationSizes getSizesPerConfiguration( + ImmutableSet sdkTargetingOptions, + ImmutableSet abiTargetingOptions, + ImmutableSet languageTargetingOptions, + ImmutableSet screenDensityTargetingOptions) { + Map minSizeByConfiguration = new HashMap<>(); + Map maxSizeByConfiguration = new HashMap<>(); + + for (SdkVersionTargeting sdkVersionTargeting : sdkTargetingOptions) { + for (AbiTargeting abiTargeting : abiTargetingOptions) { + for (ScreenDensityTargeting screenDensityTargeting : screenDensityTargetingOptions) { + for (LanguageTargeting languageTargeting : languageTargetingOptions) { + + SizeConfiguration configuration = + mergeWithDeviceSpec( + getSizeConfiguration( + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting), + getSizeRequest.getDeviceSpec()); + + long compressedSize = + getCompressedSize( + getMatchingApks( + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting)); + + minSizeByConfiguration.merge(configuration, compressedSize, Math::min); + maxSizeByConfiguration.merge(configuration, compressedSize, Math::max); + } + } + } + } + + return ConfigurationSizes.create( + /* minSizeConfigurationMap= */ ImmutableMap.copyOf(minSizeByConfiguration), + /* maxSizeConfigurationMap= */ ImmutableMap.copyOf(maxSizeByConfiguration)); + } + + protected SizeConfiguration getSizeConfiguration( + SdkVersionTargeting sdkVersionTargeting, + AbiTargeting abiTargeting, + ScreenDensityTargeting screenDensityTargeting, + LanguageTargeting languageTargeting) { + + ImmutableSet dimensions = getSizeRequest.getDimensions(); + SizeConfiguration.Builder sizeConfiguration = SizeConfiguration.builder(); + + if (dimensions.contains(SDK)) { + SizeConfiguration.getSdkName(sdkVersionTargeting).ifPresent(sizeConfiguration::setSdkVersion); + } + + if (dimensions.contains(ABI)) { + SizeConfiguration.getAbiName(abiTargeting).ifPresent(sizeConfiguration::setAbi); + } + + if (dimensions.contains(SCREEN_DENSITY)) { + SizeConfiguration.getScreenDensityName(screenDensityTargeting) + .ifPresent(sizeConfiguration::setScreenDensity); + } + + if (dimensions.contains(LANGUAGE)) { + SizeConfiguration.getLocaleName(languageTargeting).ifPresent(sizeConfiguration::setLocale); + } + + return sizeConfiguration.build(); + } + + protected DeviceSpec getDeviceSpec( + DeviceSpec deviceSpec, + SdkVersionTargeting sdkVersionTargeting, + AbiTargeting abiTargeting, + ScreenDensityTargeting screenDensityTargeting, + LanguageTargeting languageTargeting) { + + return new DeviceSpecFromTargetingBuilder(deviceSpec) + .setSdkVersion(sdkVersionTargeting) + .setSupportedAbis(abiTargeting) + .setScreenDensity(screenDensityTargeting) + .setSupportedLocales(languageTargeting) + .build(); + } + + protected SizeConfiguration mergeWithDeviceSpec( + SizeConfiguration getSizeConfiguration, DeviceSpec deviceSpec) { + + ImmutableSet dimensions = getSizeRequest.getDimensions(); + SizeConfiguration.Builder mergedSizeConfiguration = getSizeConfiguration.toBuilder(); + if (dimensions.contains(ABI) && !isAbiMissing(deviceSpec)) { + mergedSizeConfiguration.setAbi(COMMA_JOINER.join(deviceSpec.getSupportedAbisList())); + } + + if (dimensions.contains(SCREEN_DENSITY) && !isScreenDensityMissing(deviceSpec)) { + mergedSizeConfiguration.setScreenDensity(Integer.toString(deviceSpec.getScreenDensity())); + } + + if (dimensions.contains(LANGUAGE) && !isLocalesMissing(deviceSpec)) { + mergedSizeConfiguration.setLocale(COMMA_JOINER.join(deviceSpec.getSupportedLocalesList())); + } + + if (dimensions.contains(SDK) && !isSdkVersionMissing(deviceSpec)) { + mergedSizeConfiguration.setSdkVersion(String.format("%d", deviceSpec.getSdkVersion())); + } + + return mergedSizeConfiguration.build(); + } + + /** Gets the total compressed sizes represented by the APK paths. */ + private long getCompressedSize(ImmutableList apkPaths) { + return apkPaths.stream().mapToLong(apkPath -> sizeByApkPaths.get(apkPath.toString())).sum(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/AdbCommandOutputValidator.java b/src/main/java/com/android/tools/build/bundletool/device/AdbCommandOutputValidator.java new file mode 100644 index 00000000..ed4dda01 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/AdbCommandOutputValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 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.device; + +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.google.common.collect.ImmutableList; +import java.util.Optional; + +/** Validators for adb shell command outputs */ +class AdbCommandOutputValidator { + + /** Validates that ADB shell command finished successfully */ + public static void validateSuccess(ImmutableList output, String command) { + Optional successLine = output.stream().filter(s -> s.startsWith("Success")).findFirst(); + if (!successLine.isPresent()) { + throw new ParseException(String.format("adb failed: %s\nDetails:%s", command, output)); + } + } + + private AdbCommandOutputValidator() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java index f206b3a2..ead149b8 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import java.util.Collection; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -121,7 +122,8 @@ public ImmutableList getMatchingApks(BuildApksResult buildApksResult) { matchingVariant.get(), Version.of(buildApksResult.getBundletool().getVersion())) : ImmutableList.of(); - ImmutableList assetModuleApks = getMatchingApksFromAssetModules(buildApksResult); + ImmutableList assetModuleApks = + getMatchingApksFromAssetModules(buildApksResult.getAssetSliceSetList()); return ImmutableList.builder().addAll(variantApks).addAll(assetModuleApks).build(); } @@ -278,12 +280,13 @@ private void checkCompatibleWithApkTargetingHelper( matcher.checkDeviceCompatible(matcher.getTargetingValue(apkTargeting)); } - private ImmutableList getMatchingApksFromAssetModules(BuildApksResult buildApksResult) { + public ImmutableList getMatchingApksFromAssetModules( + Collection assetModules) { ImmutableList.Builder matchedApksBuilder = ImmutableList.builder(); - Predicate assetModuleNameMatcher = getAssetModuleNameMatcher(buildApksResult); + Predicate assetModuleNameMatcher = getAssetModuleNameMatcher(assetModules); - for (AssetSliceSet sliceSet : buildApksResult.getAssetSliceSetList()) { + for (AssetSliceSet sliceSet : assetModules) { String moduleName = sliceSet.getAssetModuleMetadata().getName(); for (ApkDescription apkDescription : sliceSet.getApkDescriptionList()) { ApkTargeting apkTargeting = apkDescription.getTargeting(); @@ -296,13 +299,13 @@ private ImmutableList getMatchingApksFromAssetModules(BuildApksResult b return matchedApksBuilder.build(); } - private Predicate getAssetModuleNameMatcher(BuildApksResult buildApksResult) { + private Predicate getAssetModuleNameMatcher(Collection assetModules) { if (requestedModuleNames.isPresent()) { return requestedModuleNames.get()::contains; } ImmutableSet upfrontAssetModuleNames = - buildApksResult.getAssetSliceSetList().stream() + assetModules.stream() .filter( sliceSet -> sliceSet diff --git a/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java new file mode 100644 index 00000000..3fbd03c2 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.AssetSliceSet; +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.ScreenDensityTargeting; +import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.model.ConfigurationSizes; +import com.android.tools.build.bundletool.model.GetSizeRequest; +import com.android.tools.build.bundletool.model.ZipPath; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; + +/** + * Get total size (min and max) for a list of asset modules, for each {@link SizeConfiguration} + * based on the requested dimensions passed to {@link GetSizeCommand}. + * + *

Asset module slices are filtered based on the given {@link VariantTargeting} and {@link + * DeviceSpec}. + */ +public class AssetModuleSizeAggregator extends AbstractSizeAggregator { + + private final Collection assetModules; + private final VariantTargeting variantTargeting; + + public AssetModuleSizeAggregator( + Collection assetModules, + VariantTargeting variantTargeting, + ImmutableMap sizeByApkPaths, + GetSizeRequest getSizeRequest) { + super(sizeByApkPaths, getSizeRequest); + this.assetModules = assetModules; + this.variantTargeting = variantTargeting; + } + + @Override + public ConfigurationSizes getSize() { + + ImmutableList apkDescriptions = + assetModules.stream() + .flatMap(assetModule -> assetModule.getApkDescriptionList().stream()) + .collect(toImmutableList()); + + ImmutableSet sdkVersionTargetingOptions = + variantTargeting.hasSdkVersionTargeting() + ? ImmutableSet.of(variantTargeting.getSdkVersionTargeting()) + : getAllSdkVersionTargetings(apkDescriptions); + ImmutableSet abiTargetingOptions = + variantTargeting.hasAbiTargeting() + ? ImmutableSet.of(variantTargeting.getAbiTargeting()) + : getAllAbiTargetings(apkDescriptions); + ImmutableSet languageTargetingOptions = + getAllLanguageTargetings(apkDescriptions); + ImmutableSet screenDensityTargetingOptions = + variantTargeting.hasScreenDensityTargeting() + ? ImmutableSet.of(variantTargeting.getScreenDensityTargeting()) + : getAllScreenDensityTargetings(apkDescriptions); + + return getSizesPerConfiguration( + sdkVersionTargetingOptions, + abiTargetingOptions, + languageTargetingOptions, + screenDensityTargetingOptions); + } + + @Override + protected ImmutableList getMatchingApks( + SdkVersionTargeting sdkVersionTargeting, + AbiTargeting abiTargeting, + ScreenDensityTargeting screenDensityTargeting, + LanguageTargeting languageTargeting) { + return new ApkMatcher( + getDeviceSpec( + getSizeRequest.getDeviceSpec(), + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting), + getSizeRequest.getModules(), + getSizeRequest.getInstant()) + .getMatchingApksFromAssetModules(assetModules); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/BadgingPackageNameParser.java b/src/main/java/com/android/tools/build/bundletool/device/BadgingPackageNameParser.java new file mode 100644 index 00000000..e4617ad9 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/BadgingPackageNameParser.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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.device; + +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.google.common.collect.ImmutableList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Parses the Package Name from the output of "aapt2 dump badging". */ +public final class BadgingPackageNameParser { + + private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile(".*? name='(.*?)'.*"); + + private BadgingPackageNameParser() {} + + public static String parse(ImmutableList badgingOutput) { + String packageLine = + badgingOutput.stream() + .filter(line -> line.trim().startsWith("package:")) + .findFirst() + .orElseThrow( + () -> + new ParseException( + String.format( + "'package:' line not found in badging output\n: %s", + String.join("\n", badgingOutput)))); + Matcher matcher = PACKAGE_NAME_PATTERN.matcher(packageLine); + if (!matcher.matches()) { + throw new ParseException(String.format("'name=' not found in package line: %s", packageLine)); + } + return matcher.group(1); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java index 470e5b99..0b7191c1 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.io.PrintStream; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -204,6 +205,17 @@ public void pushApks(ImmutableList apks, PushOptions pushOptions) { } } + @Override + public Path syncPackageToDevice(Path localFilePath) + throws TimeoutException, AdbCommandRejectedException, SyncException, IOException { + return Paths.get(device.syncPackageToDevice(localFilePath.toFile().getAbsolutePath())); + } + + @Override + public void removeRemotePackage(Path remoteFilePath) throws InstallException { + device.removeRemotePackage(remoteFilePath.toString()); + } + static class RemoteCommandExecutor { private final Device device; private final MultiLineReceiver receiver; diff --git a/src/main/java/com/android/tools/build/bundletool/device/Device.java b/src/main/java/com/android/tools/build/bundletool/device/Device.java index d30b470f..99bb03fe 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/Device.java +++ b/src/main/java/com/android/tools/build/bundletool/device/Device.java @@ -19,7 +19,9 @@ import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice.DeviceState; import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.InstallException; import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.SyncException; import com.android.ddmlib.TimeoutException; import com.android.sdklib.AndroidVersion; import com.google.auto.value.AutoValue; @@ -65,6 +67,11 @@ public abstract void executeShellCommand( public abstract void pushApks(ImmutableList apks, PushOptions installOptions); + public abstract Path syncPackageToDevice(Path localFilePath) + throws TimeoutException, AdbCommandRejectedException, SyncException, IOException; + + public abstract void removeRemotePackage(Path remoteFilePath) throws InstallException; + /** Options related to APK installation. */ @Immutable @AutoValue diff --git a/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java b/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java index 8a8ec55b..75501b4a 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java @@ -55,6 +55,7 @@ public DeviceSpec getDeviceSpec(Optional deviceId) { // device.getVersion().getApiLevel() returns 1 in case of failure. int deviceSdkVersion = device.getVersion().getApiLevel(); checkState(deviceSdkVersion > 1, "Error retrieving device SDK version. Please try again."); + String codename = device.getVersion().getCodename(); int deviceDensity = device.getDensity(); checkState(deviceDensity > 0, "Error retrieving device density. Please try again."); ImmutableList deviceFeatures = device.getDeviceFeatures(); @@ -73,14 +74,17 @@ public DeviceSpec getDeviceSpec(Optional deviceId) { } checkState(!supportedAbis.isEmpty(), "Error retrieving device ABIs. Please try again."); - return DeviceSpec.newBuilder() + DeviceSpec.Builder builder = DeviceSpec.newBuilder() .setSdkVersion(deviceSdkVersion) .addAllSupportedAbis(supportedAbis) .addAllSupportedLocales(deviceLocales) .setScreenDensity(deviceDensity) .addAllDeviceFeatures(deviceFeatures) - .addAllGlExtensions(glExtensions) - .build(); + .addAllGlExtensions(glExtensions); + if (codename != null) { + builder.setCodename(codename); + } + return builder.build(); } catch (TimeoutException e) { throw CommandExecutionException.builder() .withCause(e) @@ -112,7 +116,8 @@ private String getMainLocaleViaProperties(Device device) { }); } - private Device getAndValidateDevice(Optional deviceId) throws TimeoutException { + /** Gets and validates the connected device. */ + public Device getAndValidateDevice(Optional deviceId) throws TimeoutException { Device device = getTargetDevice(deviceId) .orElseThrow( diff --git a/src/main/java/com/android/tools/build/bundletool/device/MultiPackagesInstaller.java b/src/main/java/com/android/tools/build/bundletool/device/MultiPackagesInstaller.java new file mode 100644 index 00000000..6b74b1d8 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/MultiPackagesInstaller.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.android.utils.ImmutableCollectors.toImmutableList; +import static java.util.stream.Collectors.joining; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.InstallException; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.TimeoutException; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Locale; +import java.util.logging.Logger; + +/** Installer that allows to install multiple packages in the same install session on device */ +public final class MultiPackagesInstaller { + private static final Logger logger = Logger.getLogger(MultiPackagesInstaller.class.getName()); + + private final SessionIdParser sessionIdParser = new SessionIdParser(); + + private final Device device; + private final boolean enableRollback; + private final boolean noCommit; + + public MultiPackagesInstaller(Device device, boolean enableRollback, boolean noCommit) { + this.device = device; + this.enableRollback = enableRollback; + this.noCommit = noCommit; + } + + /* + * Performs install of multiple packages in the same commit session. Flow: * + * 1. Initialize multi-package session. + * 2. Install each package in its own child session. + * 3. Attach all child sessions to multi-package one. + * 4. Commit multi-package session. + */ + public void install(ImmutableListMultimap apksByPackageName) { + int parentSessionId = startParentSession(); + boolean abandonSession = true; + try { + ImmutableList childSessionIds = + apksByPackageName.keySet().stream() + .sorted() + .map( + packageName -> + installSinglePackage(packageName, apksByPackageName.get(packageName))) + .collect(toImmutableList()); + + attachChildSessionsToParent(parentSessionId, childSessionIds); + if (noCommit) { + logger.info("Abandoning install session due to 'no commit' requested."); + } + abandonSession = noCommit; + } finally { + finalizeParentSession(parentSessionId, abandonSession); + logger.info(String.format("Install %s", abandonSession ? "abandoned" : "committed")); + } + } + + private int startParentSession() { + String startSessionCommand = + String.format( + "pm install-create --multi-package --staged%s", + enableRollback ? " --enable-rollback" : ""); + return sessionIdParser.parse(executeAndValidateSuccess(device, startSessionCommand)); + } + + private void finalizeParentSession(int sessionId, boolean abandonSession) { + String finalizeSessionCommand = + String.format("pm %s %d", abandonSession ? "install-abandon" : "install-commit", sessionId); + executeAndValidateSuccess(device, finalizeSessionCommand); + } + + private int installSinglePackage(String packageName, ImmutableList apks) { + logger.info(String.format("Installing %s", packageName)); + int childSessionId = createChildSession(apks); + + for (int index = 0; index < apks.size(); index++) { + InstallableApk apkToInstall = apks.get(index); + logger.info(String.format("\tWriting %s", apkToInstall.getPath().getFileName())); + + Path remotePath = syncPackageToDevice(apkToInstall.getPath()); + try { + installRemoteApk(childSessionId, packageName + "_" + index, remotePath); + } finally { + removePackageFromDevice(remotePath); + } + } + return childSessionId; + } + + private Path syncPackageToDevice(Path apk) { + try { + return device.syncPackageToDevice(apk); + } catch (TimeoutException | AdbCommandRejectedException | SyncException | IOException e) { + throw CommandExecutionException.builder() + .withMessage("Sync APK to device has failed") + .withCause(e) + .build(); + } + } + + private void removePackageFromDevice(Path remoteApk) { + try { + device.removeRemotePackage(remoteApk); + } catch (InstallException e) { + throw CommandExecutionException.builder() + .withMessage("Package removal has failed") + .withCause(e) + .build(); + } + } + + private int createChildSession(ImmutableList apks) { + String childSessionCommand = + String.format( + "pm install-create --staged%s%s", + enableRollback ? " --enable-rollback" : "", hasApexApk(apks) ? " --apex" : ""); + return sessionIdParser.parse(executeAndValidateSuccess(device, childSessionCommand)); + } + + private void attachChildSessionsToParent( + int parentSessionId, ImmutableList childSessionIds) { + String attachCommand = + String.format( + "pm install-add-session %d %s", + parentSessionId, childSessionIds.stream().map(Object::toString).collect(joining(" "))); + executeAndValidateSuccess(device, attachCommand); + } + + private void installRemoteApk(int sessionId, String splitName, Path remoteApk) { + String installCommand = + String.format( + "pm install-write %d %s %s", sessionId, splitName, remoteApk.toAbsolutePath()); + executeAndValidateSuccess(device, installCommand); + } + + private static boolean hasApexApk(ImmutableList apks) { + return apks.stream() + .anyMatch( + apk -> + apk.getPath().getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".apex")); + } + + private static ImmutableList executeAndValidateSuccess(Device device, String command) { + ImmutableList output = new AdbShellCommandTask(device, command).execute(); + AdbCommandOutputValidator.validateSuccess(output, command); + return output; + } + + /** Represents pair of APK path and package name of this APK. */ + @AutoValue + public abstract static class InstallableApk { + + public static InstallableApk create(Path path, String packageName) { + return new AutoValue_MultiPackagesInstaller_InstallableApk(path, packageName); + } + + public abstract Path getPath(); + + public abstract String getPackageName(); + + InstallableApk() {} + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/PackagesParser.java b/src/main/java/com/android/tools/build/bundletool/device/PackagesParser.java new file mode 100644 index 00000000..56896af9 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/PackagesParser.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** Parses the output of the "pm list packages" ADB shell command. */ +public final class PackagesParser { + + /** Parses output of "pm list packages". */ + public ImmutableSet parse(ImmutableList listPackagesOutput) { + // The command lists the packages in the form + // package:com.google.a + // package:com.google.b + // ... + return listPackagesOutput.stream() + .filter(packageLine -> packageLine.contains("package:")) + .map(packageLine -> packageLine.replace("package:", "").trim()) + .collect(toImmutableSet()); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/SdkVersionMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/SdkVersionMatcher.java index e2d12213..05d765e4 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/SdkVersionMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/SdkVersionMatcher.java @@ -28,12 +28,24 @@ /** A {@link TargetingDimensionMatcher} that provides matching on SDK version. */ public final class SdkVersionMatcher extends TargetingDimensionMatcher { + private static final String RELEASE_CODENAME = "REL"; + private static final int PRE_RELEASE_SDK = 10_000; + // Most recently supported release SDK version, even on pre-release devices + // e.g. pre-releases R devices report codename "R" and sdk version 29 (Q release SDK) private final int deviceSdkVersion; + // RELEASE_CODENAME for released devices, or a codename on pre-release devices + private final String deviceCodename; public SdkVersionMatcher(DeviceSpec deviceSpec) { super(deviceSpec); deviceSdkVersion = deviceSpec.getSdkVersion(); + // Default to release device if no codename is specified. + if (deviceSpec.getCodename().isEmpty()) { + deviceCodename = RELEASE_CODENAME; + } else { + deviceCodename = deviceSpec.getCodename(); + } } @Override @@ -75,7 +87,16 @@ private boolean isBetterSdkMatch( return candidate.hasMin() && candidate.getMin().getValue() > contestedValue.getMin().getValue(); } + private boolean isPreReleaseDevice() { + return !deviceCodename.equals(RELEASE_CODENAME); + } + private boolean matchesDeviceSdk(SdkVersion value, int deviceSdkVersion) { + // If the device is pre release and the APK was compiled with pre release SDK allow install + if (isPreReleaseDevice() && value.hasMin() && value.getMin().getValue() == PRE_RELEASE_SDK) { + return true; + } + return !value.hasMin() || value.getMin().getValue() <= deviceSdkVersion; } diff --git a/src/main/java/com/android/tools/build/bundletool/device/SessionIdParser.java b/src/main/java/com/android/tools/build/bundletool/device/SessionIdParser.java new file mode 100644 index 00000000..27fb036b --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/SessionIdParser.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 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.device; + +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import java.util.Optional; + +/** Parses the output of the "pm install-create" ADB shell command. */ +class SessionIdParser { + + /** Parses sessionId from "pm install-create" command output */ + int parse(ImmutableList installCreateOutput) { + Optional successLine = + installCreateOutput.stream().filter(s -> s.startsWith("Success")).findFirst(); + if (!successLine.isPresent()) { + throw new ParseException( + String.format( + "adb: failed to parse session id from output\nDetails:%s", installCreateOutput)); + } + return parseSessionIdFromOutput(successLine.get()); + } + + private static int parseSessionIdFromOutput(String output) { + int startIndex = output.indexOf("["); + int endIndex = output.indexOf("]", startIndex); + if (startIndex == -1 || endIndex == -1) { + throw new ParseException( + String.format("adb: failed to parse session id from output\nDetails:%s", output)); + } + Integer sessionId = Ints.tryParse(output.substring(startIndex + 1, endIndex)); + if (sessionId == null) { + throw new ParseException( + String.format("adb: session id should be integer\nDetails:%s", output)); + } + return sessionId; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java index 48192f32..a3aab1c7 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.device; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.MoreCollectors.toOptional; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.BuildApksResult; @@ -24,7 +25,6 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.VariantTargeting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.MoreCollectors; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; @@ -95,7 +95,7 @@ public ImmutableList getAllMatchingVariants(BuildApksResult buildApksRe * @throws IllegalArgumentException if multiple variants are matched. */ public Optional getMatchingVariant(BuildApksResult buildApksResult) { - return getAllMatchingVariants(buildApksResult).stream().collect(MoreCollectors.toOptional()); + return getAllMatchingVariants(buildApksResult).stream().collect(toOptional()); } public void checkCompatibleWithVariant(Variant variant) { diff --git a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java index a68dfb40..9e3d5b09 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java @@ -16,68 +16,48 @@ package com.android.tools.build.bundletool.device; -import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isAbiMissing; -import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isLocalesMissing; -import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isScreenDensityMissing; -import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isSdkVersionMissing; -import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ABI; -import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.LANGUAGE; -import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SCREEN_DENSITY; -import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SDK; import static com.android.tools.build.bundletool.model.utils.ResultUtils.isStandaloneApkVariant; 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 com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.Variant; -import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.AbiTargeting; -import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; import com.android.bundle.Targeting.SdkVersionTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.commands.GetSizeCommand; -import com.android.tools.build.bundletool.device.DeviceSpecUtils.DeviceSpecFromTargetingBuilder; import com.android.tools.build.bundletool.model.ConfigurationSizes; import com.android.tools.build.bundletool.model.GetSizeRequest; -import com.android.tools.build.bundletool.model.GetSizeRequest.Dimension; import com.android.tools.build.bundletool.model.SizeConfiguration; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.version.Version; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import java.util.HashMap; -import java.util.Map; /** * Get total size (min and max) for variant, for each {@link SizeConfiguration} based on the * requested dimensions passed to {@link GetSizeCommand}. */ -public class VariantTotalSizeAggregator { +public class VariantTotalSizeAggregator extends AbstractSizeAggregator { - private static final Joiner COMMA_JOINER = Joiner.on(','); - - private final ImmutableMap sizeByApkPaths; private final Version bundleVersion; private final Variant variant; - private final GetSizeRequest getSizeRequest; public VariantTotalSizeAggregator( ImmutableMap sizeByApkPaths, Version bundleVersion, Variant variant, GetSizeRequest getSizeRequest) { - this.sizeByApkPaths = sizeByApkPaths; + super(sizeByApkPaths, getSizeRequest); this.bundleVersion = bundleVersion; this.variant = variant; - this.getSizeRequest = getSizeRequest; } + @Override public ConfigurationSizes getSize() { if (isStandaloneApkVariant(variant)) { return getSizeStandaloneVariant(); @@ -86,12 +66,32 @@ public ConfigurationSizes getSize() { } } + @Override + protected ImmutableList getMatchingApks( + SdkVersionTargeting sdkVersionTargeting, + AbiTargeting abiTargeting, + ScreenDensityTargeting screenDensityTargeting, + LanguageTargeting languageTargeting) { + return new ApkMatcher( + getDeviceSpec( + getSizeRequest.getDeviceSpec(), + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting), + getSizeRequest.getModules(), + getSizeRequest.getInstant()) + .getMatchingApksFromVariant(variant, bundleVersion); + } + private ConfigurationSizes getSizeNonStandaloneVariant() { ImmutableList apkDescriptions = variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) .collect(toImmutableList()); + ImmutableSet sdkVersionTargetingOptions = + ImmutableSet.of(variant.getTargeting().getSdkVersionTargeting()); ImmutableSet abiTargetingOptions = getAllAbiTargetings(apkDescriptions); ImmutableSet languageTargetingOptions = getAllLanguageTargetings(apkDescriptions); @@ -99,13 +99,16 @@ private ConfigurationSizes getSizeNonStandaloneVariant() { getAllScreenDensityTargetings(apkDescriptions); return getSizesPerConfiguration( - abiTargetingOptions, languageTargetingOptions, screenDensityTargetingOptions); + sdkVersionTargetingOptions, + abiTargetingOptions, + languageTargetingOptions, + screenDensityTargetingOptions); } private ConfigurationSizes getSizeStandaloneVariant() { checkState( !getSizeRequest.getInstant(), - "Standalone Variants cant be selected when instant flag is set"); + "Standalone Variants can't be selected when instant flag is set"); // When modules are specified we ignore standalone variants. if (getSizeRequest.getModules().isPresent()) { @@ -134,180 +137,4 @@ private ConfigurationSizes getSizeStandaloneVariant() { ImmutableMap.of(sizeConfiguration, compressedSize); return ConfigurationSizes.create(sizeConfigurationMap, sizeConfigurationMap); } - - private ImmutableSet getAllAbiTargetings( - ImmutableList apkDescriptions) { - ImmutableSet.Builder abiTargetingOptions = ImmutableSet.builder(); - - if (isAbiMissing(getSizeRequest.getDeviceSpec())) { - abiTargetingOptions.addAll( - apkDescriptions.stream() - .map(ApkDescription::getTargeting) - .filter(ApkTargeting::hasAbiTargeting) - .map(ApkTargeting::getAbiTargeting) - .collect(toImmutableSet())); - } - - // Adding default targeting (if targetings are empty) to help computing the cartesian product - // across all targetings. - return abiTargetingOptions.build().isEmpty() - ? ImmutableSet.of(AbiTargeting.getDefaultInstance()) - : abiTargetingOptions.build(); - } - - private ImmutableSet getAllScreenDensityTargetings( - ImmutableList apkDescriptions) { - ImmutableSet.Builder screenDensityTargetingOptions = - ImmutableSet.builder(); - - if (isScreenDensityMissing(getSizeRequest.getDeviceSpec())) { - screenDensityTargetingOptions.addAll( - apkDescriptions.stream() - .map(ApkDescription::getTargeting) - .filter(ApkTargeting::hasScreenDensityTargeting) - .map(ApkTargeting::getScreenDensityTargeting) - .collect(ImmutableSet.toImmutableSet())); - } - - // Adding default targeting (if targetings are empty) to help computing the cartesian product - // across all targetings. - return screenDensityTargetingOptions.build().isEmpty() - ? ImmutableSet.of(ScreenDensityTargeting.getDefaultInstance()) - : screenDensityTargetingOptions.build(); - } - - private ImmutableSet getAllLanguageTargetings( - ImmutableList apkDescriptions) { - ImmutableSet.Builder languageTargetingOptions = ImmutableSet.builder(); - - if (isLocalesMissing(getSizeRequest.getDeviceSpec())) { - languageTargetingOptions.addAll( - apkDescriptions.stream() - .map(ApkDescription::getTargeting) - .filter(ApkTargeting::hasLanguageTargeting) - .map(ApkTargeting::getLanguageTargeting) - .collect(ImmutableSet.toImmutableSet())); - } - - // Adding default targeting (if targetings are empty) to help computing the cartesian product - // across all targetings. - return languageTargetingOptions.build().isEmpty() - ? ImmutableSet.of(LanguageTargeting.getDefaultInstance()) - : languageTargetingOptions.build(); - } - - private ConfigurationSizes getSizesPerConfiguration( - ImmutableSet abiTargetingOptions, - ImmutableSet languageTargetingOptions, - ImmutableSet screenDensityTargetingOptions) { - Map minSizeByConfiguration = new HashMap<>(); - Map maxSizeByConfiguration = new HashMap<>(); - - SdkVersionTargeting sdkVersionTargeting = variant.getTargeting().getSdkVersionTargeting(); - for (AbiTargeting abiTargeting : abiTargetingOptions) { - for (ScreenDensityTargeting screenDensityTargeting : screenDensityTargetingOptions) { - for (LanguageTargeting languageTargeting : languageTargetingOptions) { - - SizeConfiguration configuration = - mergeWithDeviceSpec( - getSizeConfiguration( - sdkVersionTargeting, abiTargeting, screenDensityTargeting, languageTargeting), - getSizeRequest.getDeviceSpec()); - - long compressedSize = - getCompressedSize( - new ApkMatcher( - getDeviceSpec( - getSizeRequest.getDeviceSpec(), - sdkVersionTargeting, - abiTargeting, - screenDensityTargeting, - languageTargeting), - getSizeRequest.getModules(), - getSizeRequest.getInstant()) - .getMatchingApksFromVariant(variant, bundleVersion)); - - minSizeByConfiguration.merge(configuration, compressedSize, Math::min); - maxSizeByConfiguration.merge(configuration, compressedSize, Math::max); - } - } - } - - return ConfigurationSizes.create( - /* minSizeConfigurationMap= */ ImmutableMap.copyOf(minSizeByConfiguration), - /* maxSizeConfigurationMap= */ ImmutableMap.copyOf(maxSizeByConfiguration)); - } - - private SizeConfiguration getSizeConfiguration( - SdkVersionTargeting sdkVersionTargeting, - AbiTargeting abiTargeting, - ScreenDensityTargeting screenDensityTargeting, - LanguageTargeting languageTargeting) { - - ImmutableSet dimensions = getSizeRequest.getDimensions(); - SizeConfiguration.Builder sizeConfiguration = SizeConfiguration.builder(); - - if (dimensions.contains(SDK)) { - sizeConfiguration.setSdkVersion(SizeConfiguration.getSdkName(sdkVersionTargeting).orElse("")); - } - - if (dimensions.contains(ABI)) { - sizeConfiguration.setAbi(SizeConfiguration.getAbiName(abiTargeting).orElse("")); - } - - if (dimensions.contains(SCREEN_DENSITY)) { - sizeConfiguration.setScreenDensity( - SizeConfiguration.getScreenDensityName(screenDensityTargeting).orElse("")); - } - - if (dimensions.contains(LANGUAGE)) { - sizeConfiguration.setLocale(SizeConfiguration.getLocaleName(languageTargeting).orElse("")); - } - - return sizeConfiguration.build(); - } - - private DeviceSpec getDeviceSpec( - DeviceSpec deviceSpec, - SdkVersionTargeting sdkVersionTargeting, - AbiTargeting abiTargeting, - ScreenDensityTargeting screenDensityTargeting, - LanguageTargeting languageTargeting) { - - return new DeviceSpecFromTargetingBuilder(deviceSpec) - .setSdkVersion(sdkVersionTargeting) - .setSupportedAbis(abiTargeting) - .setScreenDensity(screenDensityTargeting) - .setSupportedLocales(languageTargeting) - .build(); - } - - private SizeConfiguration mergeWithDeviceSpec( - SizeConfiguration getSizeConfiguration, DeviceSpec deviceSpec) { - - ImmutableSet dimensions = getSizeRequest.getDimensions(); - SizeConfiguration.Builder mergedSizeConfiguration = getSizeConfiguration.toBuilder(); - if (dimensions.contains(ABI) && !isAbiMissing(deviceSpec)) { - mergedSizeConfiguration.setAbi(COMMA_JOINER.join(deviceSpec.getSupportedAbisList())); - } - - if (dimensions.contains(SCREEN_DENSITY) && !isScreenDensityMissing(deviceSpec)) { - mergedSizeConfiguration.setScreenDensity(Integer.toString(deviceSpec.getScreenDensity())); - } - - if (dimensions.contains(LANGUAGE) && !isLocalesMissing(deviceSpec)) { - mergedSizeConfiguration.setLocale(COMMA_JOINER.join(deviceSpec.getSupportedLocalesList())); - } - - if (dimensions.contains(SDK) && !isSdkVersionMissing(deviceSpec)) { - mergedSizeConfiguration.setSdkVersion(String.format("%d", deviceSpec.getSdkVersion())); - } - - return mergedSizeConfiguration.build(); - } - - /** Gets the total compressed sizes represented by the APK paths. */ - private long getCompressedSize(ImmutableList apkPaths) { - return apkPaths.stream().mapToLong(apkPath -> sizeByApkPaths.get(apkPath.toString())).sum(); - } } 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 146e0f23..0d41347c 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 @@ -21,6 +21,7 @@ import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; import static com.android.tools.build.bundletool.model.BundleModule.ROOT_DIRECTORY; +import static com.android.tools.build.bundletool.model.utils.files.BufferedIo.inputStream; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileHasExtension; import static com.android.tools.build.bundletool.model.utils.files.FileUtils.createParentDirectories; @@ -261,11 +262,10 @@ private Path writeProtoApk(ModuleSplit split, Path outputPath, Path tempDir) { /* uncompressNativeLibs= */ !extractNativeLibs, /* entryShouldCompress= */ entry.getShouldCompress()); if (signingConfig.isPresent() - && wear1ApkPath.isPresent() - && wear1ApkPath.get().equals(pathInApk)) { - // Sign the Wear 1.x embedded APK if there is one. - Path signedWearApk = signWearApk(entry, signingConfig.get(), tempDir); - zipBuilder.addFileFromDisk(pathInApk, signedWearApk.toFile(), entryOptions); + && (entry.getShouldSign() || wear1ApkPath.map(pathInApk::equals).orElse(false))) { + // Sign the embedded APK + Path signedApk = signEmbeddedApk(entry, signingConfig.get(), tempDir); + zipBuilder.addFileFromDisk(pathInApk, signedApk.toFile(), entryOptions); } else { zipBuilder.addFile(pathInApk, entry.getContentSupplier(), entryOptions); } @@ -335,16 +335,31 @@ private void addNonAapt2Files(ZFile zFile, ModuleSplit split) throws IOException for (ModuleEntry entry : split.getEntries()) { ZipPath pathInApk = toApkEntryPath(entry.getPath()); if (!FILES_FOR_AAPT2.apply(pathInApk)) { - try (InputStream entryInputStream = entry.getContent()) { - zFile.add( - pathInApk.toString(), - entryInputStream, - shouldCompress(pathInApk, !extractNativeLibs, entry.getShouldCompress())); + boolean shouldCompress = + shouldCompress(pathInApk, !extractNativeLibs, entry.getShouldCompress()); + if (signingConfig.isPresent() && entry.getShouldSign()) { + // Unlike ZipBuilder, ZFile copies the source stream immediately + try (TempDirectory signingDir = new TempDirectory()) { + // Sign the embedded APK + Path signedApk = signEmbeddedApk(entry, signingConfig.get(), signingDir.getPath()); + try (InputStream inputStream = inputStream(signedApk)) { + zFile.add(pathInApk.toString(), inputStream, shouldCompress); + } + } + } else { + addFile(zFile, pathInApk, entry, shouldCompress); } } } } + void addFile(ZFile zFile, ZipPath pathInApk, ModuleEntry entry, boolean shouldCompress) + throws IOException { + try (InputStream entryInputStream = entry.getContent()) { + zFile.add(pathInApk.toString(), entryInputStream, shouldCompress); + } + } + /** * Transforms the entry path in the module to the final path in the module split. * @@ -388,13 +403,14 @@ private ZipPath toApkEntryPath(ZipPath pathInModule) { } /** - * Signs the Wear APK. + * Signs an embedded APK. * - * @return the Path on disk to the signed APK. + * @return the Path on disk to the signed APK (guaranteed to be unique under {@code tempDir}). */ - private static Path signWearApk( - ModuleEntry wearApkEntry, SigningConfiguration signingConfig, Path tempDir) { - try { + private static Path signEmbeddedApk( + ModuleEntry apkEntry, SigningConfiguration signingConfig, Path tempDir) { + ZipPath targetPath = apkEntry.getPath(); + try (TempDirectory unsignedDir = new TempDirectory()) { SignerConfig signerConfig = new SignerConfig.Builder( SIGNER_CONFIG_NAME, @@ -403,29 +419,31 @@ private static Path signWearApk( .build(); // Input - Path unsignedApk = tempDir.resolve("wear-unsigned.apk"); - try (InputStream entryContent = wearApkEntry.getContent()) { + Path unsignedApk = unsignedDir.getPath().resolve("unsigned.apk"); + try (InputStream entryContent = apkEntry.getContent()) { Files.copy(entryContent, unsignedApk); } // Output - Path signedApk = tempDir.resolve("wear-signed.apk"); - + Path signedApk = Files.createTempDirectory(tempDir, "signing-").resolve("signed.apk"); ApkSigner apkSigner = new ApkSigner.Builder(ImmutableList.of(signerConfig)) .setInputApk(unsignedApk.toFile()) .setOutputApk(signedApk.toFile()) .build(); apkSigner.sign(); - return signedApk; } catch (ApkFormatException | NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - throw new ValidationException("Unable to sign the embedded Wear APK.", e); + throw ValidationException.builder() + .withCause(e) + .withMessage("Unable to sign the embedded APK '%s'.", targetPath) + .build(); } catch (IOException e) { - throw new UncheckedIOException("Unable to sign the embedded Wear APK.", e); + throw new UncheckedIOException( + String.format("Unable to sign the embedded APK '%s'.", targetPath), e); } } @@ -462,7 +480,7 @@ private static void signApk( | NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - throw new ValidationException("Unable to sign APK.", e); + throw ValidationException.builder().withCause(e).withMessage("Unable to sign APK.").build(); } } } 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 6600dfa4..d599ae0f 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 @@ -185,7 +185,8 @@ private ImmutableList serializeApks( collectingAndThen( toImmutableMap( identity(), - split -> executorService.submit(() -> apkSerializer.serialize(split))), + (ModuleSplit split) -> + executorService.submit(() -> apkSerializer.serialize(split))), ConcurrencyUtils::waitForAll)); // Build the result proto. 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 index bc59c1ac..5f142b91 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java @@ -98,8 +98,7 @@ ApkDescription writeToDiskInternal( if (standaloneSplit.isApex()) { apkDescription.setApexApkMetadata( ApexApkMetadata.newBuilder() - .addAllApexEmbeddedApkConfig( - standaloneSplit.getApexConfig().get().getApexEmbeddedApkConfigList()) + .addAllApexEmbeddedApkConfig(standaloneSplit.getApexEmbeddedApkConfigs()) .build()); } else { apkDescription.setStandaloneApkMetadata( diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java b/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java index ba8342bb..e709d360 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java @@ -124,7 +124,10 @@ public synchronized Path writeTo(Path target) throws IOException { *

Will throw an exception if the path is already taken. */ public ZipBuilder addFileWithContent(ZipPath toPath, byte[] content, EntryOption... options) { - return addFile(toPath, () -> new ByteArrayInputStream(content), options); + return addFile( + toPath, + new InputStreamSupplier(() -> new ByteArrayInputStream(content), content.length), + options); } /** @@ -144,7 +147,7 @@ public ZipBuilder addFileFromDisk(ZipPath toPath, File file, EntryOption... opti */ public ZipBuilder addFileWithProtoContent( ZipPath toPath, MessageLite protoMsg, EntryOption... options) { - return addFile(toPath, () -> new ByteArrayInputStream(protoMsg.toByteArray()), options); + return addFileWithContent(toPath, protoMsg.toByteArray(), options); } /** 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 80ac7942..42c49499 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 @@ -311,7 +311,10 @@ ModuleSplit mergeSingleApexShard(ImmutableList splitsOfShard) { /* mergedSplitType= */ SplitType.STANDALONE); // Add the APEX config as it's used to identify APEX APKs. - return shard.toBuilder().setApexConfig(splitsOfShard.get(0).getApexConfig().get()).build(); + return shard.toBuilder() + .setApexConfig(splitsOfShard.get(0).getApexConfig().get()) + .setApexEmbeddedApkConfigs(splitsOfShard.get(0).getApexEmbeddedApkConfigs()) + .build(); } private ModuleSplit buildShard( diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/SameTargetingMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/SameTargetingMerger.java index 7becd9cd..096e0a43 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/SameTargetingMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/SameTargetingMerger.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkArgument; import com.android.aapt.Resources.ResourceTable; +import com.android.bundle.Config.ApexEmbeddedApkConfig; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -67,6 +68,7 @@ private ModuleSplit mergeSplits(ImmutableCollection splits) { NativeLibraries mergedNativeConfig = null; Map mergedAssetsConfig = new HashMap<>(); ApexImages mergedApexConfig = null; + ImmutableList mergedApexEmbeddedApkConfigs = null; BundleModuleName mergedModuleName = null; Boolean mergedIsMasterSplit = null; VariantTargeting mergedVariantTargeting = null; @@ -103,6 +105,12 @@ private ModuleSplit mergeSplits(ImmutableCollection splits) { new IllegalStateException( "Encountered two distinct apex configs while merging.")); } + mergedApexEmbeddedApkConfigs = + getSameValueOrNonNull(mergedApexEmbeddedApkConfigs, split.getApexEmbeddedApkConfigs()) + .orElseThrow( + () -> + new IllegalStateException( + "Encountered two distinct apex embedded apk configs while merging.")); mergedModuleName = getSameValueOrNonNull(mergedModuleName, split.getModuleName()) .orElseThrow( @@ -149,6 +157,9 @@ private ModuleSplit mergeSplits(ImmutableCollection splits) { if (mergedApexConfig != null) { builder.setApexConfig(mergedApexConfig); } + if (mergedApexEmbeddedApkConfigs != null) { + builder.setApexEmbeddedApkConfigs(mergedApexEmbeddedApkConfigs); + } if (mergedModuleName != null) { builder.setModuleName(mergedModuleName); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/Aapt2Command.java b/src/main/java/com/android/tools/build/bundletool/model/Aapt2Command.java index 69bdbf7b..bf580a16 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/Aapt2Command.java +++ b/src/main/java/com/android/tools/build/bundletool/model/Aapt2Command.java @@ -17,8 +17,11 @@ package com.android.tools.build.bundletool.model; import com.android.tools.build.bundletool.model.utils.files.BufferedIo; +import com.google.common.collect.ImmutableList; +import com.google.common.io.CharStreams; import java.io.BufferedReader; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.Arrays; import java.util.concurrent.TimeUnit; @@ -28,6 +31,11 @@ public interface Aapt2Command { void convertApkProtoToBinary(Path protoApk, Path binaryApk); + /** Dumps the badging information of an .apk/.apex file, returning a String per line of out. */ + default ImmutableList dumpBadging(Path apkPath) { + throw new UnsupportedOperationException("Not implemented"); + } + static Aapt2Command createFromExecutablePath(Path aapt2Path) { return new Aapt2Command() { @Override @@ -42,6 +50,12 @@ public void convertApkProtoToBinary(Path protoApk, Path binaryApk) { binaryApk.toString(), protoApk.toString()); } + + @Override + public ImmutableList dumpBadging(Path apkPath) { + return new CommandExecutor() + .executeAndCapture(aapt2Path.toString(), "dump", "badging", apkPath.toString()); + } }; } @@ -50,6 +64,14 @@ class CommandExecutor { private static final int TIMEOUT_AAPT2_COMMANDS_SECONDS = 5 * 60; // 5 minutes. public void execute(String... command) { + executeImpl(command); + } + + public ImmutableList executeAndCapture(String... command) { + return captureOutput(executeImpl(command)); + } + + private static Process executeImpl(String... command) { try { Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); if (!process.waitFor(TIMEOUT_AAPT2_COMMANDS_SECONDS, TimeUnit.SECONDS)) { @@ -63,11 +85,20 @@ public void execute(String... command) { "Command '%s' didn't terminate successfully (exit code: %d). Check the logs.", Arrays.toString(command), process.exitValue())); } + return process; } catch (IOException | InterruptedException e) { throw new Aapt2Exception("Error when executing command: " + Arrays.toString(command), e); } } + private static ImmutableList captureOutput(Process process) { + try (BufferedReader outputReader = BufferedIo.reader(process.getInputStream())) { + return ImmutableList.copyOf(CharStreams.readLines(outputReader)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + private static void printOutput(Process process) { try (BufferedReader outputReader = BufferedIo.reader(process.getInputStream())) { String line; diff --git a/src/main/java/com/android/tools/build/bundletool/model/InputStreamSupplier.java b/src/main/java/com/android/tools/build/bundletool/model/InputStreamSupplier.java index 943e05be..4ff591b8 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/InputStreamSupplier.java +++ b/src/main/java/com/android/tools/build/bundletool/model/InputStreamSupplier.java @@ -16,10 +16,13 @@ package com.android.tools.build.bundletool.model; +import com.google.common.base.Optional; +import com.google.common.io.ByteSource; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.MustBeClosed; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; /** * A repeatable action returning fresh instances of {@link InputStream}. @@ -30,7 +33,51 @@ *

All subclasses must be immutable, and return the same {@link InputStream} at each invocation. */ @Immutable -public interface InputStreamSupplier { +public final class InputStreamSupplier { + + private final SupplierWithIO inputSupplier; + private final SupplierWithIO sizeSupplier; + + public InputStreamSupplier( + SupplierWithIO inputSupplier, SupplierWithIO sizeSupplier) { + this.inputSupplier = inputSupplier; + this.sizeSupplier = sizeSupplier; + } + + public InputStreamSupplier(SupplierWithIO inputSupplier, long size) { + this(inputSupplier, () -> size); + } + @MustBeClosed - InputStream get() throws IOException; + public InputStream get() throws IOException { + return inputSupplier.get(); + } + + public long sizeBytes() throws IOException { + return sizeSupplier.get(); + } + + public ByteSource asByteSource() { + return new ByteSource() { + @Override + public InputStream openStream() throws IOException { + return inputSupplier.get(); + } + + @Override + public Optional sizeIfKnown() { + try { + return Optional.of(sizeSupplier.get()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }; + } + + /** A version of {@code Supplier} that can throw an IOException. */ + @Immutable + public interface SupplierWithIO { + T get() throws IOException; + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/InputStreamSuppliers.java b/src/main/java/com/android/tools/build/bundletool/model/InputStreamSuppliers.java index 69883281..2fe8fec7 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/InputStreamSuppliers.java +++ b/src/main/java/com/android/tools/build/bundletool/model/InputStreamSuppliers.java @@ -46,7 +46,8 @@ public static InputStreamSupplier fromZipEntry(ZipEntry zipEntry, ZipFile zipFil /** Create an in-memory {@link InputStreamSupplier} from {@code contents}. */ public static InputStreamSupplier fromBytes(byte[] contents) { final byte[] contentsCopy = Arrays.copyOf(contents, contents.length); - return () -> new ByteArrayInputStream(contentsCopy); + return new InputStreamSupplier( + () -> new ByteArrayInputStream(contentsCopy), contentsCopy.length); } /** 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 da166472..653a2cfc 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,7 @@ import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.google.auto.value.AutoValue; +import com.google.common.io.ByteSource; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.MustBeClosed; import java.io.IOException; @@ -51,9 +52,16 @@ public final InputStream getContent() { /** Returns whether entry should be compressed in generated archives. */ public abstract boolean getShouldCompress(); + /** Returns whether entry is an embedded APK that should be signed by the output APK key. */ + public abstract boolean getShouldSign(); + /** Returns data source for this entry. */ public abstract InputStreamSupplier getContentSupplier(); + public ByteSource asByteSource() { + return getContentSupplier().asByteSource(); + } + /** Checks whether the given entries are identical. */ @Override public final boolean equals(Object obj2) { @@ -76,6 +84,10 @@ public final boolean equals(Object obj2) { return false; } + if (entry1.getShouldSign() != entry2.getShouldSign()) { + return false; + } + try (InputStream inputStream1 = entry1.getContent(); InputStream inputStream2 = entry2.getContent()) { return FileUtils.equalContent(inputStream1, inputStream2); @@ -96,7 +108,7 @@ public final int hashCode() { public abstract Builder toBuilder(); public static Builder builder() { - return new AutoValue_ModuleEntry.Builder().setShouldCompress(true); + return new AutoValue_ModuleEntry.Builder().setShouldCompress(true).setShouldSign(false); } /** Builder for {@code ModuleEntry}. */ @@ -106,6 +118,8 @@ public abstract static class Builder { public abstract Builder setShouldCompress(boolean shouldCompress); + public abstract Builder setShouldSign(boolean shouldSign); + /** * Sets the data source for this entry. * 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 789364fa..b04b4f3d 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 @@ -34,6 +34,7 @@ import static com.google.common.collect.MoreCollectors.toOptional; import com.android.aapt.Resources.ResourceTable; +import com.android.bundle.Config.ApexEmbeddedApkConfig; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -134,6 +135,8 @@ public enum SplitType { /** The module APEX configuration - what system images it contains and with what targeting. */ public abstract Optional getApexConfig(); + public abstract ImmutableList getApexEmbeddedApkConfigs(); + public abstract Builder toBuilder(); /** Returns true iff this is split of the base module. */ @@ -248,8 +251,7 @@ private static String formatGlVersion(OpenGlVersion glVersion) { public static ImmutableList filterResourceEntries( ImmutableList entries, ResourceTable resourceTable) { ImmutableSet referencedPaths = ResourcesUtils.getAllFileReferences(resourceTable); - return entries - .stream() + return entries.stream() .filter(entry -> referencedPaths.contains(entry.getPath())) .collect(toImmutableList()); } @@ -363,7 +365,8 @@ private String getSplitIdForMasterSplit() { public static Builder builder() { return new AutoValue_ModuleSplit.Builder() .setEntries(ImmutableList.of()) - .setSplitType(SplitType.SPLIT); + .setSplitType(SplitType.SPLIT) + .setApexEmbeddedApkConfigs(ImmutableList.of()); } /** @@ -515,6 +518,9 @@ private static ModuleSplit fromBundleModule( .setMasterSplit(true) .setSplitType(getSplitTypeFromModuleType(bundleModule.getModuleType())) .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setApexEmbeddedApkConfigs( + ImmutableList.copyOf( + bundleModule.getBundleConfig().getApexConfig().getApexEmbeddedApkConfigList())) .setVariantTargeting(variantTargeting); bundleModule.getNativeConfig().ifPresent(splitBuilder::setNativeConfig); @@ -603,6 +609,9 @@ public abstract static class Builder { */ public abstract Builder setApexConfig(ApexImages apexConfig); + public abstract Builder setApexEmbeddedApkConfigs( + ImmutableList apexEmbeddedApkConfigs); + protected abstract ApkTargeting getApkTargeting(); public abstract Builder setApkTargeting(ApkTargeting targeting); diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java index 32d5c974..414504ed 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java @@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; -import com.android.bundle.Config.ApexConfig; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -209,13 +208,11 @@ public NativeLibraries generateTargetingForNativeLibraries(Collection li * @return Targeting for all APEX image files. */ public ApexImages generateTargetingForApexImages( - ApexConfig apexConfig, Collection apexImageFiles, boolean hasBuildInfo) { + Collection apexImageFiles, boolean hasBuildInfo) { ImmutableMap targetingByPath = Maps.toMap(apexImageFiles, path -> buildMultiAbi(path.getFileName().toString())); - ApexImages.Builder apexImages = - ApexImages.newBuilder() - .addAllApexEmbeddedApkConfig(apexConfig.getApexEmbeddedApkConfigList()); + ApexImages.Builder apexImages = ApexImages.newBuilder(); ImmutableSet allTargeting = ImmutableSet.copyOf(targetingByPath.values()); targetingByPath.forEach( (imagePath, targeting) -> diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtils.java index 459258b0..b6328582 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtils.java @@ -38,7 +38,7 @@ public class ApkSizeUtils { * Returns a map of APK Paths inside the APK Set with the sizes, for all APKs in variants * provided. */ - public static ImmutableMap getCompressedSizeByApkPaths( + public static ImmutableMap getVariantCompressedSizeByApkPaths( ImmutableList variants, Path apksArchive) { ImmutableList apkPaths = variants.stream() @@ -47,6 +47,11 @@ public static ImmutableMap getCompressedSizeByApkPaths( .map(ApkDescription::getPath) .distinct() .collect(toImmutableList()); + return getCompressedSizeByApkPaths(apkPaths, apksArchive); + } + + public static ImmutableMap getCompressedSizeByApkPaths( + ImmutableList apkPaths, Path apksArchive) { ImmutableMap.Builder sizeByApkPath = ImmutableMap.builder(); try (ZipFile apksZip = new ZipFile(apksArchive.toFile())) { for (String apkPath : apkPaths) { @@ -64,4 +69,6 @@ public static ImmutableMap getCompressedSizeByApkPaths( } return sizeByApkPath.build(); } + + private ApkSizeUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java new file mode 100644 index 00000000..d1025a82 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 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.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.android.tools.build.bundletool.model.ConfigurationSizes; +import com.android.tools.build.bundletool.model.SizeConfiguration; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.util.Map; +import java.util.Optional; + +/** + * Utility to merge two {@link ConfigurationSizes} by combining all compatible entries. + * + *

The resulting sizes for matching configurations are added together. if the configurations + * target different dimensions, new combinations will be generated using the cartesian product.of + * the disjoint dimensions. + */ +public class ConfigurationSizesMerger { + + /** Merges two {@link ConfigurationSizes} */ + public static ConfigurationSizes merge(ConfigurationSizes config1, ConfigurationSizes config2) { + return ConfigurationSizes.create( + mergeSizeConfigurationMap( + config1.getMinSizeConfigurationMap(), config2.getMinSizeConfigurationMap()), + mergeSizeConfigurationMap( + config1.getMaxSizeConfigurationMap(), config2.getMaxSizeConfigurationMap())); + } + + private static ImmutableMap mergeSizeConfigurationMap( + ImmutableMap map1, ImmutableMap map2) { + return map1.entrySet().stream() + .flatMap( + entry1 -> + map2.entrySet().stream() + .filter(entry2 -> areCompatible(entry1.getKey(), entry2.getKey())) + .map(entry2 -> combineEntries(entry1, entry2))) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static boolean areCompatible( + SizeConfiguration sizeConfig1, SizeConfiguration sizeConfig2) { + + return areCompatible(sizeConfig1.getAbi(), sizeConfig2.getAbi()) + && areCompatible(sizeConfig1.getLocale(), sizeConfig2.getLocale()) + && areCompatible(sizeConfig1.getScreenDensity(), sizeConfig2.getScreenDensity()) + && areCompatible(sizeConfig1.getSdkVersion(), sizeConfig2.getSdkVersion()); + } + + /** + * Checks whether two values for a single dimension are compatible. + * + *

This happens if they have the same value or any of them is absent. + */ + private static boolean areCompatible(Optional value1, Optional value2) { + return value1.equals(value2) || !value1.isPresent() || !value2.isPresent(); + } + + /** + * Combine two compatible entries by merging the size configuration and adding the compressed + * sizes. + */ + private static Map.Entry combineEntries( + Map.Entry entry1, Map.Entry entry2) { + checkState( + areCompatible(entry1.getKey(), entry2.getKey()), + "Tried to combine incompatible size configurations."); + SizeConfiguration.Builder configBuilder = entry1.getKey().toBuilder(); + entry2.getKey().getAbi().ifPresent(configBuilder::setAbi); + entry2.getKey().getLocale().ifPresent(configBuilder::setLocale); + entry2.getKey().getScreenDensity().ifPresent(configBuilder::setScreenDensity); + entry2.getKey().getSdkVersion().ifPresent(configBuilder::setSdkVersion); + return Maps.immutableEntry(configBuilder.build(), entry1.getValue() + entry2.getValue()); + } + + private ConfigurationSizesMerger() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocator.java b/src/main/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocator.java index a46982e1..79cf9a84 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocator.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocator.java @@ -30,6 +30,7 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.Path; @@ -245,7 +246,7 @@ private Optional locateBinaryOnSystemPath( if (binaryInDir.isPresent()) { return binaryInDir; } - } catch (NoSuchFileException | NotDirectoryException tolerate) { + } catch (NoSuchFileException | NotDirectoryException | InvalidPathException tolerate) { // Tolerate invalid PATH entries. } catch (IOException e) { throw CommandExecutionException.builder() diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java index 9bf64939..24f445bf 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java @@ -19,9 +19,7 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; import static com.google.common.base.Predicates.not; -import com.android.tools.build.bundletool.model.InputStreamSupplier; import com.android.tools.build.bundletool.model.ZipPath; -import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.common.io.CountingOutputStream; import java.io.IOException; @@ -37,9 +35,6 @@ /** Misc utilities for working with zip files. */ public final class ZipUtils { - // See {@link GZIPOutputStream#writeHeader}. - private static final long GZIP_HEADER_SIZE = 10L; - public static Stream allFileEntriesPaths(ZipFile zipFile) { return allFileEntries(zipFile).map(zipEntry -> ZipPath.create(zipEntry.getName())); } @@ -68,33 +63,6 @@ public static long calculateGzipCompressedSize(@WillNotClose InputStream stream) return countingOutputStream.getCount(); } - /** - * Given a list of {@link InputStreamSupplier} passes those streams through a {@link - * GZIPOutputStream} and computes the GZIP size increments attributed to each stream. - */ - public static ImmutableList calculateGZipSizeForEntries( - ImmutableList streams) throws IOException { - ImmutableList.Builder gzipSizeIncrements = ImmutableList.builder(); - CountingOutputStream countingOutputStream = - new CountingOutputStream(ByteStreams.nullOutputStream()); - long lastOffset = GZIP_HEADER_SIZE; - // We need to use syncFlush which is slower but allows us to accurately count GZIP bytes. - // See {@link Deflater#SYNC_FLUSH}. Sync-flush flushes all deflater's pending output upon - // calling flush(). - try (GZIPOutputStream compressedStream = - new GZIPOutputStream(countingOutputStream, /* syncFlush= */ true)) { - for (InputStreamSupplier stream : streams) { - try (InputStream is = stream.get()) { - ByteStreams.copy(is, compressedStream); - compressedStream.flush(); - gzipSizeIncrements.add(countingOutputStream.getCount() - lastOffset); - lastOffset = countingOutputStream.getCount(); - } - } - } - return gzipSizeIncrements.build(); - } - /** * Converts a path relative to the bundle zip root to one relative to the module path. * diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/files/BufferedIo.java b/src/main/java/com/android/tools/build/bundletool/model/utils/files/BufferedIo.java index 9ddae667..b5977b5d 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/files/BufferedIo.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/files/BufferedIo.java @@ -62,13 +62,13 @@ public static InputStream inputStream(@WillNotClose ZipFile zipFile, ZipEntry zi @SuppressWarnings("MustBeClosedChecker") // InputStreamSupplier is annotated with @MustBeClosed public static InputStreamSupplier inputStreamSupplier(Path file) { - return () -> inputStream(file); + return new InputStreamSupplier(() -> inputStream(file), () -> Files.size(file)); } @SuppressWarnings("MustBeClosedChecker") // InputStreamSupplier is annotated with @MustBeClosed public static InputStreamSupplier inputStreamSupplier( @WillNotClose ZipFile zipFile, ZipEntry zipEntry) { - return () -> inputStream(zipFile, zipEntry); + return new InputStreamSupplier(() -> inputStream(zipFile, zipEntry), () -> zipEntry.getSize()); } @MustBeClosed 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 90bfeb47..0ce516a1 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 = "0.13.4"; + private static final String CURRENT_VERSION = "0.14.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 764b46cd..18cdb067 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 @@ -68,7 +68,18 @@ public enum VersionGuardedFeature { * merged from features to base module by Gradle plugin * (https://github.com/google/bundletool/issues/68). */ - FUSE_ACTIVITIES_FROM_FEATURE_MANIFESTS("0.13.4"); + FUSE_ACTIVITIES_FROM_FEATURE_MANIFESTS("0.13.4"), + + /** + * Requires to put bucket with the lowest density for each style into master split. This allows to + * fix crashes on Android pre P devices which are unable to use styles that are defined only in + * config splits without having any value in master one. + * + *

When a style is available in master split it can be overridden by config splits for specific + * density that's why having only the lowest density value in master split and every other value + * in config splits is enough (https://github.com/google/bundletool/issues/128). + */ + PIN_LOWEST_DENSITY_OF_EACH_STYLE_TO_MASTER("0.14.0"); /** Version from which the given feature should be enabled by default. */ private final Version 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 new file mode 100644 index 00000000..cc8ce59b --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/EmbeddedApkSigningPreprocessor.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2020 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.preprocessors; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Config.UnsignedEmbeddedApkConfig; +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.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.Set; +import javax.annotation.CheckReturnValue; + +/** Identify embedded APKs which should be signed with the same key as generated APKs. */ +public class EmbeddedApkSigningPreprocessor implements AppBundlePreprocessor { + @Override + public AppBundle preprocess(AppBundle bundle) { + ImmutableSet unsignedEmbeddedApkPaths = + bundle.getBundleConfig().getUnsignedEmbeddedApkConfigList().stream() + .map(UnsignedEmbeddedApkConfig::getPath) + .map(ZipPath::create) + .collect(toImmutableSet()); + ImmutableSet.Builder foundApkPaths = ImmutableSet.builder(); + + AppBundle appBundle = + bundle.toBuilder() + .setRawModules( + setShouldSign( + bundle.getModules().values(), unsignedEmbeddedApkPaths, foundApkPaths)) + .build(); + + Set missingApks = Sets.difference(unsignedEmbeddedApkPaths, foundApkPaths.build()); + if (!missingApks.isEmpty()) { + throw ValidationException.builder() + .withMessage( + "Unsigned embedded APKs specified in bundle config but missing from bundle: %s", + missingApks) + .build(); + } + + return appBundle; + } + + @CheckReturnValue + private static ImmutableList setShouldSign( + ImmutableCollection modules, + ImmutableSet unsignedEmbeddedApkPaths, + ImmutableSet.Builder foundApkPaths) { + return modules.stream() + .map(module -> setShouldSign(module, unsignedEmbeddedApkPaths, foundApkPaths)) + .collect(toImmutableList()); + } + + private static BundleModule setShouldSign( + BundleModule module, + ImmutableSet unsignedEmbeddedApkPaths, + ImmutableSet.Builder foundApkPaths) { + return module.toBuilder() + .setRawEntries( + module.getEntries().stream() + .map(entry -> setShouldSign(entry, unsignedEmbeddedApkPaths, foundApkPaths)) + .collect(toImmutableList())) + .build(); + } + + private static ModuleEntry setShouldSign( + ModuleEntry moduleEntry, + ImmutableSet unsignedEmbeddedApkPaths, + ImmutableSet.Builder foundApkPaths) { + boolean shouldSign = unsignedEmbeddedApkPaths.contains(moduleEntry.getPath()); + if (shouldSign) { + foundApkPaths.add(moduleEntry.getPath()); + } + return moduleEntry.toBuilder().setShouldSign(shouldSign).build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/size/ApkBreakdownGenerator.java b/src/main/java/com/android/tools/build/bundletool/size/ApkBreakdownGenerator.java index 54780e9c..6b062631 100644 --- a/src/main/java/com/android/tools/build/bundletool/size/ApkBreakdownGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/size/ApkBreakdownGenerator.java @@ -16,7 +16,6 @@ package com.android.tools.build.bundletool.size; -import static com.android.tools.build.bundletool.model.utils.ZipUtils.calculateGZipSizeForEntries; import static com.android.tools.build.bundletool.size.SizeUtils.addSizes; import static com.android.tools.build.bundletool.size.SizeUtils.sizes; import static com.android.tools.build.bundletool.size.SizeUtils.subtractSizes; @@ -26,11 +25,13 @@ import com.android.bundle.SizesOuterClass.Breakdown; import com.android.bundle.SizesOuterClass.Sizes; import com.android.tools.build.bundletool.model.InputStreamSupplier; +import com.android.tools.build.bundletool.model.InputStreamSuppliers; import com.android.tools.build.bundletool.model.utils.ZipUtils; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Streams; import java.io.BufferedInputStream; +import java.io.Closeable; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -39,12 +40,18 @@ import java.util.AbstractMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** Calculates a breakdown of a single APK. */ public class ApkBreakdownGenerator { + // Each time we add an entry to the deflater a syncronization entry is added. + // This would not be present when we acually compress the APK for serving, it's just an artifact + // of flushing after each file. + static final int DEFLATER_SYNC_OVERHEAD_BYTES = 5; + public static Breakdown calculateBreakdown(Path apkPath) throws IOException { try (ZipFile apk = new ZipFile(apkPath.toFile())) { ImmutableMap downloadSizeByEntry = calculateDownloadSizePerEntry(apk); @@ -110,9 +117,7 @@ private static ImmutableMap calculateDownloadSizePerEntry(ZipFile ImmutableList streams = zipFile.stream() - .map( - zipStreamEntry -> - (InputStreamSupplier) () -> zipFile.getInputStream(zipStreamEntry)) + .map(zipStreamEntry -> InputStreamSuppliers.fromZipEntry(zipStreamEntry, zipFile)) .collect(toImmutableList()); ImmutableList downloadSizes = calculateGZipSizeForEntries(streams); @@ -121,4 +126,50 @@ private static ImmutableMap calculateDownloadSizePerEntry(ZipFile .collect( toImmutableMap(entry -> entry.getKey().getName(), AbstractMap.SimpleEntry::getValue)); } + + /** + * Given a list of {@link InputStreamSupplier} passes those streams through a {@link + * GZIPOutputStream} and computes the GZIP size increments attributed to each stream. + */ + public static ImmutableList calculateGZipSizeForEntries( + ImmutableList streams) throws IOException { + ImmutableList.Builder gzipSizeIncrements = ImmutableList.builder(); + + Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, /* noWrap */ true); + + try (Closeable unused = () -> deflater.end()) { + // matches the {@code ByteStreams} buffer size + byte[] inputBuffer = new byte[8192]; + + // Worse case overestimate for the max size deflation should result it. + // (most of the time deflation should result in a smaller output, but there are cases + // where it can be larger). + byte[] outputBuffer = new byte[2 * inputBuffer.length]; + + for (InputStreamSupplier stream : streams) { + try (InputStream is = stream.get()) { + long gzipSize = 0; + while (true) { + int r = is.read(inputBuffer); + if (r == -1) { + // We need to use syncFlush which is slower but allows us to accurately count GZIP + // bytes. See {@link Deflater#SYNC_FLUSH}. Sync-flush flushes all deflater's pending + // output upon calling flush(). + gzipSize += + deflater.deflate(outputBuffer, 0, outputBuffer.length, Deflater.SYNC_FLUSH); + gzipSizeIncrements.add(Math.max(0, gzipSize - DEFLATER_SYNC_OVERHEAD_BYTES)); + break; + } + deflater.setInput(inputBuffer, 0, r); + while (!deflater.needsInput()) { + gzipSize += deflater.deflate(outputBuffer, 0, outputBuffer.length, Deflater.NO_FLUSH); + } + } + } + } + } + return gzipSizeIncrements.build(); + } + + private ApkBreakdownGenerator() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java b/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java index 5bd5b47d..9ebdf492 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java @@ -261,7 +261,8 @@ private SplittingPipeline createResourcesSplittingPipeline( new ScreenDensityResourcesSplitter( bundleVersion, /* pinWholeResourceToMaster= */ Predicates.alwaysFalse(), - /* pinLowestBucketOfResourceToMaster= */ Predicates.alwaysFalse())); + /* pinLowestBucketOfResourceToMaster= */ Predicates.alwaysFalse(), + /* pinLowestBucketOfStylesToMaster= */ false)); } if (shardingDimensions.contains(OptimizationDimension.LANGUAGE) diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java index aa846491..e3f5fe7b 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java @@ -18,6 +18,7 @@ import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.PIN_LOWEST_DENSITY_OF_EACH_STYLE_TO_MASTER; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -271,7 +272,8 @@ private SplittingPipeline createResourcesSplittingPipeline() { new ScreenDensityResourcesSplitter( bundleVersion, /* pinWholeResourceToMaster= */ masterPinnedResourceIds::contains, - /* pinLowestBucketOfResourceToMaster= */ baseManifestReachableResources::contains)); + /* pinLowestBucketOfResourceToMaster= */ baseManifestReachableResources::contains, + PIN_LOWEST_DENSITY_OF_EACH_STYLE_TO_MASTER.enabledForVersion(bundleVersion))); } if (apkGenerationConfiguration diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java index 2414dbf9..52a178e2 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java @@ -69,31 +69,38 @@ public class ScreenDensityResourcesSplitter extends SplitterForOneTargetingDimen DensityAlias.XXXHDPI, DensityAlias.TVDPI); + private static final String STYLE_TYPE_NAME = "style"; + private final ImmutableSet densityBuckets; private final Version bundleVersion; private final Predicate pinWholeResourceToMaster; private final Predicate pinLowestBucketOfResourceToMaster; + private final boolean pinLowestBucketOfStylesToMaster; public ScreenDensityResourcesSplitter( Version bundleVersion, Predicate pinWholeResourceToMaster, - Predicate pinLowestBucketOfResourceToMaster) { + Predicate pinLowestBucketOfResourceToMaster, + boolean pinLowestBucketOfStylesToMaster) { this( DEFAULT_DENSITY_BUCKETS, bundleVersion, pinWholeResourceToMaster, - pinLowestBucketOfResourceToMaster); + pinLowestBucketOfResourceToMaster, + pinLowestBucketOfStylesToMaster); } public ScreenDensityResourcesSplitter( ImmutableSet densityBuckets, Version bundleVersion, Predicate pinWholeResourceToMaster, - Predicate pinLowestBucketOfResourceToMaster) { + Predicate pinLowestBucketOfResourceToMaster, + boolean pinLowestBucketOfStylesToMaster) { this.densityBuckets = densityBuckets; this.bundleVersion = bundleVersion; this.pinWholeResourceToMaster = pinWholeResourceToMaster; this.pinLowestBucketOfResourceToMaster = pinLowestBucketOfResourceToMaster; + this.pinLowestBucketOfStylesToMaster = pinLowestBucketOfStylesToMaster; } @Override @@ -245,7 +252,7 @@ private Entry filterEntryForDensity(ResourceTableEntry tableEntry, DensityAlias Predicate pinConfigToMaster; if (pinWholeResourceToMaster.test(tableEntry.getResourceId())) { pinConfigToMaster = anyConfig -> true; - } else if (pinLowestBucketOfResourceToMaster.test(tableEntry.getResourceId())) { + } else if (pinLowestBucketToMaster(tableEntry)) { ImmutableSet lowDensityConfigsPinnedToMaster = pickBestDensityForEachGroup(densityGroups, getLowestDensity(densityBuckets)) .collect(toImmutableSet()); @@ -261,6 +268,11 @@ private Entry filterEntryForDensity(ResourceTableEntry tableEntry, DensityAlias return initialEntry.toBuilder().clearConfigValue().addAllConfigValue(valuesToKeep).build(); } + private boolean pinLowestBucketToMaster(ResourceTableEntry entry) { + return pinLowestBucketOfResourceToMaster.test(entry.getResourceId()) + || (pinLowestBucketOfStylesToMaster && STYLE_TYPE_NAME.equals(entry.getType().getName())); + } + /** For each density group, it picks the best match for a given desired densityAlias. */ private Stream pickBestDensityForEachGroup( ImmutableList> densityGroups, DensityAlias densityAlias) { diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java index 1e0c1905..033c2da7 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java @@ -93,7 +93,8 @@ public void validateModuleFile(ZipPath file) { throw new InvalidNativeLibraryPathException(LIB_DIRECTORY, file); } - if (!fileName.endsWith(".so")) { + // See https://developer.android.com/ndk/guides/wrap-script#packaging_wrapsh + if (!fileName.endsWith(".so") && !fileName.equals("wrap.sh")) { throw new InvalidFileExtensionInDirectoryException(LIB_DIRECTORY, ".so", file); } diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index 484b3bcd..f644feab 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -11,6 +11,8 @@ message BundleConfig { // Resources to be always kept in the master split. MasterResources master_resources = 4; ApexConfig apex_config = 5; + // APKs to be signed with the same key as generated APKs. + repeated UnsignedEmbeddedApkConfig unsigned_embedded_apk_config = 6; } message Bundletool { @@ -126,3 +128,9 @@ message ApexEmbeddedApkConfig { // Path to the APK within the APEX system image. string path = 2; } + +message UnsignedEmbeddedApkConfig { + // Path to the APK inside the module (e.g. if the path inside the bundle + // is split/assets/example.apk, this will be assets/example.apk). + string path = 1; +} diff --git a/src/main/proto/devices.proto b/src/main/proto/devices.proto index cbb003c3..f9fea870 100644 --- a/src/main/proto/devices.proto +++ b/src/main/proto/devices.proto @@ -22,5 +22,9 @@ message DeviceSpec { // Screen dpi. uint32 screen_density = 5; + // getprop ro.build.version.sdk uint32 sdk_version = 6; + + // getprop ro.build.version.codename + string codename = 7; } diff --git a/src/main/proto/files.proto b/src/main/proto/files.proto index 59c0b849..7d3bf3f4 100644 --- a/src/main/proto/files.proto +++ b/src/main/proto/files.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package android.bundle; -import "config.proto"; import "targeting.proto"; option java_package = "com.android.bundle"; @@ -23,11 +22,10 @@ message NativeLibraries { // Describes the APEX images in the App Bundle. message ApexImages { + reserved 2; + // List of all the image files under "apex/". repeated TargetedApexImage image = 1; - - // Configuration for processing of APKs embedded in an APEX image. - repeated ApexEmbeddedApkConfig apex_embedded_apk_config = 2; } // An assets directory in the module that contains targeting information. 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 ccf99c5f..ac79805f 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 @@ -1412,10 +1412,9 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro "base", builder -> builder - .setManifest(androidManifest("com.test.app")) - .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .setManifest(androidManifest("com.test.app"))) .addModule( - "feature_tcf_assets", + "tcf_assets", builder -> builder .addFile("assets/textures#tcf_atc/texture.dat") @@ -1430,9 +1429,8 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro assetsDirectoryTargeting( textureCompressionTargeting(ETC1_RGB8))))) .setManifest( - androidManifestForFeature( - "com.test.app", - withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -1454,7 +1452,7 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro ApkDescription universalApk = apkDescriptions(universalVariant).get(0); - // All assets from "feature_tcf_assets" are included inside the APK + // All assets from "tcf_assets" are included inside the APK File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); try (ZipFile universalApkZipFile = new ZipFile(universalApkFile)) { assertThat(filesUnderPath(universalApkZipFile, ASSETS_DIRECTORY)) @@ -1463,6 +1461,78 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro } } + @Test + public void buildApksCommand_universal_generatesSingleApkWithSuffixStrippedTcfAssets() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .setManifest(androidManifest("com.test.app"))) + .addModule( + "tcf_assets", + builder -> + builder + .addFile("assets/textures#tcf_atc/texture.dat") + .addFile("assets/textures#tcf_etc1/texture.dat") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/textures#tcf_atc", + assetsDirectoryTargeting(textureCompressionTargeting(ATC))), + targetedAssetsDirectory( + "assets/textures#tcf_etc1", + assetsDirectoryTargeting( + textureCompressionTargeting(ETC1_RGB8))))) + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension( + Value.TEXTURE_COMPRESSION_FORMAT, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "etc1") + .build()) + .build(); + Path bundlePath = createAndStoreBundle(appBundle); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setApkBuildMode(UNIVERSAL) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = openZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(standaloneApkVariants(result)).hasSize(1); + + Variant universalVariant = standaloneApkVariants(result).get(0); + assertThat(apkDescriptions(universalVariant)).hasSize(1); + + ApkDescription universalApk = apkDescriptions(universalVariant).get(0); + + // Only assets from "tcf_assets" which are etc1 should be included, + // and the targeting suffix stripped. + File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); + try (ZipFile universalApkZipFile = new ZipFile(universalApkFile)) { + assertThat(filesUnderPath(universalApkZipFile, ASSETS_DIRECTORY)) + .containsExactly("assets/textures/texture.dat"); + } + + // Check that targeting was applied to both the APK and the variant + assertThat( + universalVariant.getTargeting().getTextureCompressionFormatTargeting().getValueList()) + .containsExactly(textureCompressionFormat(ETC1_RGB8)); + assertThat(universalApk.getTargeting().getTextureCompressionFormatTargeting().getValueList()) + .containsExactly(textureCompressionFormat(ETC1_RGB8)); + } @Test public void buildApksCommand_universal_strip64BitLibraries_doesNotStrip() throws Exception { @@ -1523,6 +1593,67 @@ public void buildApksCommand_universal_strip64BitLibraries_doesNotStrip() throws } } + @Test + public void buildApksCommand_universal_withAssetModules() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("lib/x86/libsome.so") + .setManifest(androidManifest("com.test.app"))) + .addModule( + "upfront_asset_module", + builder -> + builder + .addFile("assets/upfront_asset.jpg") + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) + .addModule( + "on_demand_asset_module", + builder -> + builder + .addFile("assets/on_demand_asset.jpg") + .setManifest( + androidManifestForAssetModule("com.test.app", withOnDemandDelivery()))) + .build(); + Path bundlePath = createAndStoreBundle(appBundle); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setApkBuildMode(UNIVERSAL) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = openZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + assertThat(result.getVariantList()).hasSize(1); + assertThat(splitApkVariants(result)).isEmpty(); + assertThat(standaloneApkVariants(result)).hasSize(1); + + Variant universalVariant = standaloneApkVariants(result).get(0); + assertThat(universalVariant.getTargeting()).isEqualTo(UNRESTRICTED_VARIANT_TARGETING); + + assertThat(apkDescriptions(universalVariant)).hasSize(1); + ApkDescription universalApk = apkDescriptions(universalVariant).get(0); + assertThat(universalApk.getTargeting()).isEqualToDefaultInstance(); + + File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); + try (ZipFile universalApkZipFile = new ZipFile(universalApkFile)) { + assertThat(filesUnderPath(universalApkZipFile, ZipPath.create("lib"))) + .containsExactly("lib/x86/libsome.so"); + assertThat(filesUnderPath(universalApkZipFile, ZipPath.create("assets"))) + .containsExactly( + "assets/upfront_asset.jpg"); + } + } + @Test public void buildApksCommand_compressedSystem_generatesSingleApkWithEmptyOptimizations() throws Exception { diff --git a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java index da1d6cab..f70a834d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java @@ -32,6 +32,7 @@ import static com.android.tools.build.bundletool.model.utils.ZipUtils.calculateGzipCompressedSize; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksArchiveFile; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createAssetSliceSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createInstantApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSplitApkSet; @@ -42,7 +43,9 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkSdkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; @@ -54,6 +57,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.Variant; @@ -675,11 +679,11 @@ public void getSizeTotalInternal_multipleDimensions() throws Exception { ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), - ZipPath.create("base-mdpi.apk"), + ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), - ZipPath.create("base-ldpi.apk"), + ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false))); Variant preLVariant = standaloneVariant( @@ -715,22 +719,16 @@ public void getSizeTotalInternal_multipleDimensions() throws Exception { SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("LDPI") - .setAbi("") - .setLocale("") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("MDPI") - .setAbi("") - .setLocale("") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("15-20") .setAbi("armeabi") - .setScreenDensity("") - .setLocale("") .build(), compressedApkSize); assertThat(configurationSizes.getMaxSizeConfigurationMap()) @@ -738,22 +736,16 @@ public void getSizeTotalInternal_multipleDimensions() throws Exception { SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("LDPI") - .setAbi("") - .setLocale("") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("MDPI") - .setAbi("") - .setLocale("") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("15-20") .setAbi("armeabi") - .setScreenDensity("") - .setLocale("") .build(), compressedApkSize); } @@ -769,11 +761,11 @@ public void getSizeTotal_noDimensions() throws Exception { ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), - ZipPath.create("base-mdpi.apk"), + ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), - ZipPath.create("base-ldpi.apk"), + ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false)), createSplitApkSet( /* moduleName= */ "feature1", @@ -996,11 +988,11 @@ public void getSizeTotal_withDimensionsAndDeviceSpec() throws Exception { ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), - ZipPath.create("base-mdpi.apk"), + ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), - ZipPath.create("base-ldpi.apk"), + ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86, ImmutableSet.of(X86_64)), @@ -1050,6 +1042,222 @@ public void getSizeTotal_withDimensionsAndDeviceSpec() throws Exception { + CRLF); } + @Test + public void getSizeTotal_withAssetModules() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + createApkDescription( + apkAbiTargeting(X86, ImmutableSet.of(X86_64)), + ZipPath.create("base-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(X86_64, ImmutableSet.of(X86)), + ZipPath.create("base-x86_64.apk"), + /* isMasterSplit= */ false))); + + AssetSliceSet assetModule = + createAssetSliceSet( + /* moduleName= */ "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")), + createApkDescription( + apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), + ZipPath.create("asset1-ldpi.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), + ZipPath.create("asset1-mdpi.apk"), + /* isMasterSplit= */ false)); + // Only install-time asset modules are counted towards the size. + AssetSliceSet ignoredOnDemandAssetModule = + createAssetSliceSet( + /* moduleName= */ "asset2", + DeliveryType.ON_DEMAND, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset2-master.apk"))); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .addAssetSliceSet(assetModule) + .addAssetSliceSet(ignoredOnDemandAssetModule) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDimensions(ImmutableSet.of(Dimension.ABI, Dimension.SCREEN_DENSITY, Dimension.SDK)) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) + .asList() + .containsExactly( + "SDK,ABI,SCREEN_DENSITY,MIN,MAX", + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86_64", "MDPI", 4 * compressedApkSize, 4 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86_64", "LDPI", 4 * compressedApkSize, 4 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86", "MDPI", 4 * compressedApkSize, 4 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86", "LDPI", 4 * compressedApkSize, 4 * compressedApkSize)); + } + + @Test + public void getSizeTotal_withAssetModulesAndDeviceSpec() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + createApkDescription( + apkAbiTargeting(X86, ImmutableSet.of(X86_64)), + ZipPath.create("base-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(X86_64, ImmutableSet.of(X86)), + ZipPath.create("base-x86_64.apk"), + /* isMasterSplit= */ false))); + + AssetSliceSet assetModule = + createAssetSliceSet( + /* moduleName= */ "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")), + createApkDescription( + apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), + ZipPath.create("asset1-ldpi.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), + ZipPath.create("asset1-mdpi.apk"), + /* isMasterSplit= */ false)); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .addAssetSliceSet(assetModule) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDimensions(ImmutableSet.of(Dimension.ABI, Dimension.SCREEN_DENSITY, Dimension.SDK)) + .setDeviceSpec( + DeviceSpec.newBuilder() + .setSdkVersion(25) + .addSupportedAbis("x86") + .setScreenDensity(125) + .build()) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + assertThat(new String(outputStream.toByteArray(), UTF_8)) + .isEqualTo( + "SDK,ABI,SCREEN_DENSITY,MIN,MAX" + + CRLF + + String.format( + "%s,%s,%s,%d,%d", + "25", "x86", "125", 4 * compressedApkSize, 4 * compressedApkSize) + + CRLF); + } + + @Test + public void getSizeTotal_withAssetModulesAndMultipleVariants() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")))); + Variant nVariant = + createVariant( + variantSdkTargeting(24), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master_2.apk")))); + + AssetSliceSet assetModule = + createAssetSliceSet( + /* moduleName= */ "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset1-master.apk")), + createApkDescription( + mergeApkTargeting( + apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset1-ldpi.apk"), + /* isMasterSplit= */ false), + createApkDescription( + mergeApkTargeting( + apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset1-mdpi.apk"), + /* isMasterSplit= */ false)); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .addVariant(nVariant) + .addAssetSliceSet(assetModule) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDimensions(ImmutableSet.of(Dimension.SCREEN_DENSITY, Dimension.ABI, Dimension.SDK)) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) + .asList() + .containsExactly( + "SDK,ABI,SCREEN_DENSITY,MIN,MAX", + String.format( + "%s,,%s,%d,%d", "21-", "LDPI", 3 * compressedApkSize, 3 * compressedApkSize), + String.format( + "%s,,%s,%d,%d", "21-", "MDPI", 3 * compressedApkSize, 3 * compressedApkSize), + String.format( + "%s,,%s,%d,%d", "24-", "LDPI", 3 * compressedApkSize, 3 * compressedApkSize), + String.format( + "%s,,%s,%d,%d", "24-", "MDPI", 3 * compressedApkSize, 3 * compressedApkSize)); + } + /** Copies the testdata resource into the temporary directory. */ private Path copyToTempDir(String testDataPath) throws Exception { Path testDataFilename = Paths.get(testDataPath).getFileName(); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java new file mode 100644 index 00000000..10e6878f --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java @@ -0,0 +1,844 @@ +/* + * Copyright (C) 2020 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.utils.Versions.ANDROID_Q_API_VERSION; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.apexVariant; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.apkDescriptionStream; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksArchiveFile; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSplitApkSet; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; +import static com.android.tools.build.bundletool.testing.DeviceFactory.qDeviceWithLocales; +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.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static java.util.stream.Collectors.joining; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Config.Bundletool; +import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.SdkVersion; +import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmlib.TimeoutException; +import com.android.tools.build.bundletool.device.AdbServer; +import com.android.tools.build.bundletool.device.Device; +import com.android.tools.build.bundletool.device.IncompatibleDeviceException; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.Aapt2Command; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.version.BundleToolVersion; +import com.android.tools.build.bundletool.testing.FakeAdbServer; +import com.android.tools.build.bundletool.testing.FakeDevice; +import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Int32Value; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +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 InstallMultiApksCommandTest { + private static final Integer PARENT_SESSION_ID = 1111111; + private static final String DEVICE_ID = "id1"; + private static final String PKG_NAME_1 = "com.example.a"; + private static final String PKG_NAME_2 = "com.example.b"; + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + private Path tmpDir; + private SystemEnvironmentProvider systemEnvironmentProvider; + private Path adbPath; + private Path sdkDirPath; + private FakeDevice device; + + @Before + public void setUp() throws IOException { + tmpDir = tmp.getRoot().toPath(); + sdkDirPath = Files.createDirectory(tmpDir.resolve("android-sdk")); + adbPath = sdkDirPath.resolve("platform-tools").resolve("adb"); + Files.createDirectories(adbPath.getParent()); + Files.createFile(adbPath); + adbPath.toFile().setExecutable(true); + systemEnvironmentProvider = + new FakeSystemEnvironmentProvider( + ImmutableMap.of(ANDROID_HOME, sdkDirPath.toString(), ANDROID_SERIAL, DEVICE_ID)); + device = FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, qDeviceWithLocales("en-US")); + } + + @Test + public void fromFlags_matchBuilder_apksZip() { + Path zipFile = tmpDir.resolve("container.zip"); + + InstallMultiApksCommand fromFlags = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipFile), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US"))); + + InstallMultiApksCommand fromBuilder = + InstallMultiApksCommand.builder() + .setAdbPath(adbPath) + .setAdbServer(fromFlags.getAdbServer()) + .setApksArchiveZipPath(zipFile) + .setDeviceId(DEVICE_ID) + .build(); + + assertThat(fromBuilder).isEqualTo(fromFlags); + } + + @Test + public void execute_badAdbPath() { + Path zipFile = tmpDir.resolve("container.zip"); + + InstallMultiApksCommand fromFlags = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--adb=foo", "--apks-zip=" + zipFile), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US"))); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, fromFlags::execute); + assertThat(e).hasMessageThat().contains("was not found"); + } + + @Test + public void fromFlags_enableRollback() { + Path zipFile = tmpDir.resolve("container.zip"); + + InstallMultiApksCommand fromFlags = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--enable-rollback", "--apks-zip=" + zipFile), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US"))); + + assertThat(fromFlags.getEnableRollback()).isTrue(); + } + + @Test + public void fromFlags_deviceId() { + Path zipFile = tmpDir.resolve("container.zip"); + + InstallMultiApksCommand fromFlags = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--device-id=" + DEVICE_ID, "--apks-zip=" + zipFile), + new FakeSystemEnvironmentProvider( + ImmutableMap.of( + ANDROID_HOME, sdkDirPath.toString(), ANDROID_SERIAL, "other-device-id")), + fakeServerOneDevice(qDeviceWithLocales("en-US"))); + + assertThat(fromFlags.getDeviceId()).hasValue(DEVICE_ID); + } + + @Test + public void fromFlags_apks() throws Exception { + Path apkFile1 = tmpDir.resolve("file1.apks"); + Path apkFile2 = tmpDir.resolve("file2.apks"); + Files.createFile(apkFile1); + Files.createFile(apkFile2); + + InstallMultiApksCommand fromFlags = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + apkFile1 + "," + apkFile2), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US"))); + + assertThat(fromFlags.getApksArchivePaths()).containsExactly(apkFile1, apkFile2); + } + + @Test + public void fromFlags_exclusiveApksOptions() { + Path apkDir = tmpDir.resolve("apk_dir"); + Path apkFile1 = apkDir.resolve("file1.apks"); + Path apkFile2 = apkDir.resolve("file2.apks"); + Path zipFile = tmpDir.resolve("container.zip"); + + CommandExecutionException e = + assertThrows( + CommandExecutionException.class, + () -> + InstallMultiApksCommand.fromFlags( + new FlagParser() + .parse( + String.format("--apks=%s,%s", apkFile1, apkFile2), + "--apks-zip=" + zipFile), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US")))); + assertThat(e).hasMessageThat().contains("Exactly one of"); + } + + @Test + public void fromFlags_missingApksOption() { + CommandExecutionException e = + assertThrows( + CommandExecutionException.class, + () -> + InstallMultiApksCommand.fromFlags( + new FlagParser().parse(), + systemEnvironmentProvider, + fakeServerOneDevice(qDeviceWithLocales("en-US")))); + assertThat(e).hasMessageThat().contains("Exactly one of"); + } + + @Test + public void execute_extractZip() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + // EXPECT + // 1) parent session creation + device.injectShellCommandOutput( + "pm install-create --multi-package --staged", () -> "Success: blah blah [1111111]"); + // 2) child session creation + AtomicInteger childSessionCounter = new AtomicInteger(); + device.injectShellCommandOutput( + "pm install-create --staged", + () -> "Success: blah blah [" + childSessionCounter.getAndIncrement() + "]"); + // 3) apk writes + device.injectShellCommandOutput( + String.format("pm install-write 0 com.example.a_0 %s", pushedFileName("base-master.apk")), + () -> "Success"); + device.injectShellCommandOutput( + String.format( + "pm install-write 0 com.example.a_1 %s", pushedFileName("feature1-master.apk")), + () -> "Success"); + device.injectShellCommandOutput( + String.format( + "pm install-write 0 com.example.a_2 %s", pushedFileName("feature2-master.apk")), + () -> "Success"); + device.injectShellCommandOutput( + String.format("pm install-write 1 com.example.b_0 %s", pushedFileName("base-master.apk")), + () -> "Success"); + device.injectShellCommandOutput( + String.format( + "pm install-write 1 com.example.b_1 %s", pushedFileName("feature1-master.apk")), + () -> "Success"); + device.injectShellCommandOutput( + String.format( + "pm install-write 1 com.example.b_2 %s", pushedFileName("feature2-master.apk")), + () -> "Success"); + // 4) Adding child to parent + device.injectShellCommandOutput("pm install-add-session 1111111 0 1", () -> "Success"); + // 5) Commit. + device.injectShellCommandOutput("pm install-commit 1111111", () -> "Success"); + command.execute(); + } + + @Test + public void execute_ignoreEmptyCommandResponseLines() throws Exception { + // GIVEN a zip file containing fake .apks files for multiple packages. + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + // GIVEN the success message on the parent session includes empty lines. + device.injectShellCommandOutput( + "pm install-create --multi-package --staged", () -> "Success: blah blah [1111111]\n\n"); + + // EXPECT processing to continue normally. + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + givenInstallWrites(device, 1, PKG_NAME_2, tableOfContent2); + givenInstallAddAndCommit(device, ImmutableList.of(0, 1)); + command.execute(); + } + + @Test + public void execute_updateOnly() throws Exception { + // GIVEN a zip file containing fake .apks files for multiple packages. + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + // GIVEN only one of the packages is installed on the device... + device.injectShellCommandOutput( + "pm list packages", () -> String.format("package:%s\njunk_to_ignore", PKG_NAME_1)); + + // GIVEN the --update-only flag is set on the command... + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle, "--update-only"), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallAddAndCommit(device, ImmutableList.of(0)); + + // EXPECT only one of the packages + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + command.execute(); + } + + @Test + public void execute_installNewPackages() throws Exception { + // GIVEN a zip file containing fake .apks files for multiple packages. + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + // GIVEN the --update-only flag is not set. + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallAddAndCommit(device, ImmutableList.of(0, 1)); + + // EXPECT + // both packages to be installed. + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + givenInstallWrites(device, 1, PKG_NAME_2, tableOfContent2); + + // ACT + // Make sure the package list was not retrieved since the --update-only flag was not set. + device.injectShellCommandOutput( + "pm list packages", + () -> { + throw new IllegalStateException("Package list should not be called"); + }); + + command.execute(); + } + + @Test + public void execute_noCommit() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + // GIVEN a command with --no-commit + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle, "--no-commit"), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + givenInstallWrites(device, 1, PKG_NAME_2, tableOfContent2); + device.injectShellCommandOutput("pm install-add-session 1111111 0 1", () -> "Success"); + + // EXPECT + // Abandon session instead of committing, even though all prior commands succeeded. + device.injectShellCommandOutput("pm install-abandon 1111111", () -> "Success"); + command.execute(); + } + + @Test + public void execute_gracefulExitIfNoPackagesFound() throws Exception { + // GIVEN a zip file containing fake .apks files for multiple packages. + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + // GIVEN only no packages are installed on the device... + device.injectShellCommandOutput("pm list packages", () -> ""); + + // GIVEN the --update-only flag is used to restrict to previously installed packages. + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle, "--update-only"), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + // EXPECT no further commands to be executed. + command.execute(); + } + + @Test + public void execute_skipUnsupportedSdks() throws Exception { + // GIVEN a .apks containing containing only targets that are greater than the device SDK... + BuildApksResult apexTableOfContents = + BuildApksResult.newBuilder() + .setPackageName(PKG_NAME_1) + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + apexVariant( + VariantTargeting.newBuilder() + .setSdkVersionTargeting( + SdkVersionTargeting.newBuilder() + .addValue( + SdkVersion.newBuilder() + .setMin(Int32Value.of(ANDROID_Q_API_VERSION + 3))) + .build()) + .build(), + ApkTargeting.getDefaultInstance(), + ZipPath.create("base.apex"))) + .build(); + Path package1Apks = createApksArchiveFile(apexTableOfContents, tmpDir.resolve("package1.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + package1Apks), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + // THEN the command executes without triggering any shell commands. + command.execute(); + } + + @Test + public void execute_processApex() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult apexTableOfContents = + BuildApksResult.newBuilder() + .setPackageName(PKG_NAME_2) + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + apexVariant( + VariantTargeting.getDefaultInstance(), + ApkTargeting.getDefaultInstance(), + ZipPath.create("base.apex"))) + .build(); + Path package2Apks = createApksArchiveFile(apexTableOfContents, tmpDir.resolve("package2.apks")); + ZipBuilder bundleBuilder = new ZipBuilder(); + bundleBuilder + .addFileFromDisk(ZipPath.create("package1.apks"), package1Apks.toFile()) + .addFileFromDisk(ZipPath.create("package2.apks"), package2Apks.toFile()); + Path zipBundle = bundleBuilder.writeTo(tmpDir.resolve("bundle.zip")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks-zip=" + zipBundle), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + givenInstallAddAndCommit(device, ImmutableList.of(0, 1)); + + // EXPECT + // Apex session and write + device.injectShellCommandOutput( + "pm install-create --staged --apex", () -> "Success: blah blah [1]"); + device.injectShellCommandOutput( + String.format("pm install-write 1 com.example.b_0 %s", pushedFileName("base.apex")), + () -> "Success"); + command.execute(); + } + + @Test + public void execute_packageNameMissing_fallBackOnAapt2() throws Exception { + // GIVEN a fake .apks file without a package name. + BuildApksResult tableOfContents1 = + fakeTableOfContents(PKG_NAME_1).toBuilder().clearPackageName().build(); + Path apksPath = createApksArchiveFile(tableOfContents1, tmpDir.resolve("package1.apks")); + + // GIVEN a command with a fake aapt2 command... + Aapt2Command aapt2Command = + createFakeAapt2Command( + () -> + ImmutableList.of( + String.format( + "package: name='%s' versionCode='292200000' versionName=''" + + " platformBuildVersionName='' platformBuildVersionCode=''" + + " compileSdkVersion='29' compileSdkVersionCodename='10'", + PKG_NAME_1), + "application: label='' icon=''", + "sdkVersion:'29'", + "maxSdkVersion:'29'", + "")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.builder() + .setAdbServer(fakeServerOneDevice(device)) + .setDeviceId(DEVICE_ID) + .setAdbPath(adbPath) + .addApksArchivePath(apksPath) + .setAapt2Command(aapt2Command) + .build(); + + // EXPECT the command to execute successfully. + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContents1); + givenInstallAddAndCommit(device, ImmutableList.of(0)); + command.execute(); + } + + @Test + public void execute_packageNameMissing_aapt2Failure() throws Exception { + // GIVEN a fake .apks file without a package name, and another with a package name... + BuildApksResult tableOfContents1 = + fakeTableOfContents(PKG_NAME_1).toBuilder().clearPackageName().build(); + Path apksPath1 = createApksArchiveFile(tableOfContents1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContents2 = fakeTableOfContents(PKG_NAME_2); + Path apksPath2 = createApksArchiveFile(tableOfContents2, tmpDir.resolve("package2.apks")); + + // GIVEN a command with a fake aapt2 command that will fail with an IncompatibleDeviceException. + Aapt2Command aapt2Command = + createFakeAapt2Command( + () -> { + throw new IncompatibleDeviceException("Invalid device"); + }); + + InstallMultiApksCommand command = + InstallMultiApksCommand.builder() + .setAdbServer(fakeServerOneDevice(device)) + .setDeviceId(DEVICE_ID) + .setAdbPath(adbPath) + .setApksArchivePaths(ImmutableList.of(apksPath1, apksPath2)) + .setAapt2Command(aapt2Command) + .build(); + + // EXPECT the command to skip the incompatible package. + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_2, tableOfContents2); + givenInstallAddAndCommit(device, ImmutableList.of(0)); + command.execute(); + } + + @Test + public void execute_enableRollback() throws Exception { + // GIVEN a fake .apks file with an .apex file. + BuildApksResult apexTableOfContents = + BuildApksResult.newBuilder() + .setPackageName(PKG_NAME_1) + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + apexVariant( + VariantTargeting.getDefaultInstance(), + ApkTargeting.getDefaultInstance(), + ZipPath.create("base.apex"))) + .build(); + Path apksPath = createApksArchiveFile(apexTableOfContents, tmpDir.resolve("package1.apks")); + + // GIVEN an install command with the --enable-rollback flag... + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + apksPath, "--enable-rollback"), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenInstallWrites(device, 0, PKG_NAME_1, ImmutableList.of("base.apex")); + givenInstallAddAndCommit(device, ImmutableList.of(0)); + + // EXPECT + // 1) parent session creation with rollback + device.injectShellCommandOutput( + "pm install-create --multi-package --staged --enable-rollback", + () -> "Success: blah blah [1111111]"); + // 2) child session creation with rollback + device.injectShellCommandOutput( + "pm install-create --staged --enable-rollback --apex", () -> "Success: blah blah [0]"); + + command.execute(); + } + + @Test + public void execute_apkList_handleFailureException() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContent2 = fakeTableOfContents(PKG_NAME_2); + Path package2Apks = createApksArchiveFile(tableOfContent2, tmpDir.resolve("package2.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse(String.format("--apks=%s,%s", package1Apks, package2Apks)), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, ImmutableList.of("base-master.apk")); + + // ACT + // Simulate a timeout exception. + device.injectShellCommandOutput( + String.format( + "pm install-write 0 com.example.a_1 %s", pushedFileName("feature1-master.apk")), + () -> { + throw new TimeoutException("Timeout"); + }); + + // EXPECT + // Abandon the parent session. + device.injectShellCommandOutput("pm install-abandon 1111111", () -> "Success"); + CommandExecutionException e = assertThrows(CommandExecutionException.class, command::execute); + assertThat(e).hasMessageThat().contains("Timeout"); + assertThat(e).hasCauseThat().isInstanceOf(TimeoutException.class); + } + + @Test + public void execute_apkList_handleMalformedSuccess() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + package1Apks), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + + // ACT + // Simulate a *malformed* child session creation + device.injectShellCommandOutput("pm install-create --staged", () -> "Success: blah blah"); + + // EXPECT + // Abandon the parent session. + device.injectShellCommandOutput("pm install-abandon 1111111", () -> "Success"); + ParseException e = assertThrows(ParseException.class, command::execute); + assertThat(e).hasMessageThat().contains("failed to parse session id from output"); + } + + @Test + public void execute_apkList_handleSessionFailure() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + package1Apks), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + + // ACT + // Simulate a *failed* child session creation + device.injectShellCommandOutput("pm install-create --staged", () -> "Failed"); + + // EXPECT + // Abandon the parent session + device.injectShellCommandOutput("pm install-abandon 1111111", () -> "Success"); + CommandExecutionException e = assertThrows(CommandExecutionException.class, command::execute); + assertThat(e).hasMessageThat().contains("adb failed: pm install-create --staged"); + } + + @Test + public void execute_apkList_handleAddSessionFailure() throws Exception { + // GIVEN a zip file containing fake .apks files + BuildApksResult tableOfContent1 = fakeTableOfContents(PKG_NAME_1); + Path package1Apks = createApksArchiveFile(tableOfContent1, tmpDir.resolve("package1.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.fromFlags( + new FlagParser().parse("--apks=" + package1Apks), + systemEnvironmentProvider, + fakeServerOneDevice(device)); + + givenParentSessionCreation(device); + givenChildSessionCreate(device); + givenInstallWrites(device, 0, PKG_NAME_1, tableOfContent1); + + // ACT + // Simulate *fail* on add session + device.injectShellCommandOutput("pm install-add-session 1111111 0", () -> "Failure"); + + // EXPECT + // Abandon the parent session + device.injectShellCommandOutput("pm install-abandon 1111111", () -> "Success"); + CommandExecutionException e = assertThrows(CommandExecutionException.class, command::execute); + assertThat(e).hasMessageThat().contains("install-add-session"); + } + + private static BuildApksResult fakeTableOfContents(String packageName) { + return BuildApksResult.newBuilder() + .setPackageName(packageName) + .setBundletool( + Bundletool.newBuilder().setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk"))), + createSplitApkSet( + "feature1", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("feature1-master.apk"))), + createSplitApkSet( + "feature2", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("feature2-master.apk"))))) + .build(); + } + + private static String pushedFileName(String fileName) { + return Paths.get("/temp", fileName).toAbsolutePath().toString(); + } + + private static AdbServer fakeServerOneDevice(DeviceSpec deviceSpec) { + return new FakeAdbServer( + /* hasInitialDeviceList= */ true, + ImmutableList.of(FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, deviceSpec))); + } + + private static AdbServer fakeServerOneDevice(Device device) { + return new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(device)); + } + + // Utility methods for populating shell commands + // + // NOTE: these methods replicate the logic under test, and therefore should be used with + // caution. Expected changes per unit test should be injected manually. + + private static void givenParentSessionCreation(FakeDevice device) { + device.injectShellCommandOutput( + "pm install-create --multi-package --staged", + () -> String.format("Success: blah blah [%d]", PARENT_SESSION_ID)); + } + + private static void givenInstallWrites( + FakeDevice device, int sessionId, String packageName, BuildApksResult toc) { + ImmutableList fileNames = + apkDescriptionStream(toc) + .map(ApkDescription::getPath) + .map(Paths::get) + .map(Path::getFileName) + .map(Path::toString) + .collect(toImmutableList()); + givenInstallWrites(device, sessionId, packageName, fileNames); + } + + private static void givenInstallWrites( + FakeDevice device, int sessionId, String packageName, ImmutableList fileNames) { + for (int i = 0; i < fileNames.size(); i++) { + device.injectShellCommandOutput( + String.format( + "pm install-write %d %s_%d %s", + sessionId, packageName, i, pushedFileName(fileNames.get(i))), + () -> "Success"); + } + } + + private static void givenInstallAddAndCommit( + FakeDevice device, ImmutableList childSessionIds) { + device.injectShellCommandOutput( + String.format( + "pm install-add-session %d %s", + PARENT_SESSION_ID, + childSessionIds.stream().map(i -> Integer.toString(i)).collect(joining(" "))), + () -> "Success"); + device.injectShellCommandOutput( + String.format("pm install-commit %d", PARENT_SESSION_ID), () -> "Success"); + } + + private static void givenChildSessionCreate(FakeDevice device) { + AtomicInteger childSessionCounter = new AtomicInteger(); + device.injectShellCommandOutput( + "pm install-create --staged", + () -> "Success: blah blah [" + childSessionCounter.getAndIncrement() + "]"); + } + + private static Aapt2Command createFakeAapt2Command( + Supplier> dumpBadgingSupplier) { + return new Aapt2Command() { + @Override + public void convertApkProtoToBinary(Path protoApk, Path binaryApk) { + throw new UnsupportedOperationException(); + } + + @Override + public ImmutableList dumpBadging(Path apkPath) { + return dumpBadgingSupplier.get(); + } + }; + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java b/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java new file mode 100644 index 00000000..934801aa --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ABI; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SDK; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApkDescription; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createAssetSliceSet; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; +import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkVersion; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkSdkTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; +import static com.google.common.truth.Truth.assertThat; + +import com.android.bundle.Commands.AssetSliceSet; +import com.android.bundle.Commands.DeliveryType; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.commands.GetSizeCommand; +import com.android.tools.build.bundletool.commands.GetSizeCommand.GetSizeSubcommand; +import com.android.tools.build.bundletool.model.ConfigurationSizes; +import com.android.tools.build.bundletool.model.SizeConfiguration; +import com.android.tools.build.bundletool.model.ZipPath; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AssetModuleSizeAggregatorTest { + + private static final long ASSET_1_MASTER_SIZE = 1 << 0; + private static final long ASSET_1_X86_SIZE = 1 << 1; + private static final long ASSET_1_X8664_SIZE = 1 << 2; + private static final long ASSET_2_MASTER_SIZE = 1 << 3; + private static final long ASSET_2_X86_SIZE = 1 << 4; + private static final long ASSET_2_X8664_SIZE = 1 << 5; + private static final AssetSliceSet ASSET_MODULE_1 = + createAssetSliceSet( + "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset1-master.apk")), + createApkDescription( + mergeApkTargeting( + apkAbiTargeting(X86, ImmutableSet.of(X86_64)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset1-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + mergeApkTargeting( + apkAbiTargeting(X86_64, ImmutableSet.of(X86)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset1-x86_64.apk"), + /* isMasterSplit= */ false)); + private static final AssetSliceSet ASSET_MODULE_2 = + createAssetSliceSet( + "asset2", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset2-master.apk")), + createApkDescription( + mergeApkTargeting( + apkAbiTargeting(X86, ImmutableSet.of(X86_64)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset2-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + mergeApkTargeting( + apkAbiTargeting(X86_64, ImmutableSet.of(X86)), + apkSdkTargeting(sdkVersionFrom(21))), + ZipPath.create("asset2-x86_64.apk"), + /* isMasterSplit= */ false)); + private static final ImmutableMap SIZE_BY_APK_PATHS = + ImmutableMap.builder() + .put("asset1-master.apk", ASSET_1_MASTER_SIZE) + .put("asset1-x86.apk", ASSET_1_X86_SIZE) + .put("asset1-x86_64.apk", ASSET_1_X8664_SIZE) + .put("asset2-master.apk", ASSET_2_MASTER_SIZE) + .put("asset2-x86.apk", ASSET_2_X86_SIZE) + .put("asset2-x86_64.apk", ASSET_2_X8664_SIZE) + .build(); + + private final GetSizeCommand.Builder getSizeCommand = + GetSizeCommand.builder() + .setApksArchivePath(Paths.get("dummy.apks")) + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL); + + @Test + public void getSize_noAssetModules() throws Exception { + ConfigurationSizes configurationSizes = + new AssetModuleSizeAggregator( + ImmutableList.of(), + VariantTargeting.getDefaultInstance(), + ImmutableMap.of(), + getSizeCommand.build()) + .getSize(); + assertThat(configurationSizes.getMinSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 0L); + assertThat(configurationSizes.getMaxSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 0L); + } + + @Test + public void getSize_singleAssetModule_noTargeting() throws Exception { + ImmutableList assetModules = + ImmutableList.of( + createAssetSliceSet( + "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")))); + VariantTargeting variantTargeting = VariantTargeting.getDefaultInstance(); + ImmutableMap sizeByApkPaths = ImmutableMap.of("asset1-master.apk", 10L); + ConfigurationSizes configurationSizes = + new AssetModuleSizeAggregator( + assetModules, variantTargeting, sizeByApkPaths, getSizeCommand.build()) + .getSize(); + assertThat(configurationSizes.getMinSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 10L); + assertThat(configurationSizes.getMaxSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 10L); + } + + @Test + public void getSize_multipleAssetModules_withTargeting() throws Exception { + ImmutableList assetModules = ImmutableList.of(ASSET_MODULE_1, ASSET_MODULE_2); + VariantTargeting variantTargeting = variantSdkTargeting(21); + ConfigurationSizes configurationSizes = + new AssetModuleSizeAggregator( + assetModules, + variantTargeting, + SIZE_BY_APK_PATHS, + getSizeCommand.setDimensions(ImmutableSet.of(ABI)).build()) + .getSize(); + assertThat(configurationSizes.getMinSizeConfigurationMap()) + .containsExactly( + SizeConfiguration.builder().setAbi("x86").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X86_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X86_SIZE, + SizeConfiguration.builder().setAbi("x86_64").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X8664_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X8664_SIZE); + assertThat(configurationSizes.getMaxSizeConfigurationMap()) + .containsExactly( + SizeConfiguration.builder().setAbi("x86").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X86_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X86_SIZE, + SizeConfiguration.builder().setAbi("x86_64").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X8664_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X8664_SIZE); + } + + @Test + public void getSize_multipleAssetModules_withDeviceSpecAndVariantTargeting() throws Exception { + ImmutableList assetModules = ImmutableList.of(ASSET_MODULE_1, ASSET_MODULE_2); + VariantTargeting variantTargeting = variantAbiTargeting(X86); + ConfigurationSizes configurationSizes = + new AssetModuleSizeAggregator( + assetModules, + variantTargeting, + SIZE_BY_APK_PATHS, + getSizeCommand + .setDimensions(ImmutableSet.of(ABI, SDK)) + .setDeviceSpec(sdkVersion(21)) + .build()) + .getSize(); + assertThat(configurationSizes.getMinSizeConfigurationMap()) + .containsExactly( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X86_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X86_SIZE); + assertThat(configurationSizes.getMaxSizeConfigurationMap()) + .containsExactly( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21").build(), + ASSET_1_MASTER_SIZE + ASSET_1_X86_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_X86_SIZE); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/BadgingPackageNameParserTest.java b/src/test/java/com/android/tools/build/bundletool/device/BadgingPackageNameParserTest.java new file mode 100644 index 00000000..df33b4d5 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/BadgingPackageNameParserTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BadgingPackageNameParserTest { + + @Test + public void parse_matchingCase() { + // GIVEN an input with a package name... + ImmutableList input = + ImmutableList.of( + "package: name='com.google.android.media.swcodec' versionCode='292200000'" + + " versionName='' platformBuildVersionName='' platformBuildVersionCode=''" + + " compileSdkVersion='29' compileSdkVersionCodename='10'", + "application: label='' icon=''", + "sdkVersion:'29'", + "maxSdkVersion:'29'", + ""); + + // WHEN parsed + String packageName = BadgingPackageNameParser.parse(input); + + // THEN the correct package name is returned. + assertThat(packageName).isEqualTo("com.google.android.media.swcodec"); + } + + @Test + public void parse_missingPackageLine() { + // GIVEN an input without a package line... + ImmutableList input = + ImmutableList.of( + "application: label='' icon=''", "sdkVersion:'29'", "maxSdkVersion:'29'", ""); + + // WHEN parsed + ParseException e = + assertThrows(ParseException.class, () -> BadgingPackageNameParser.parse(input)); + + // THEN an IllegalArgumentException is thrown. + assertThat(e).hasMessageThat().contains("line not found in badging"); + } + + @Test + public void parse_missingPackageName() { + // GIVEN an input without a package name... + ImmutableList input = + ImmutableList.of( + "package: versionCode='292200000'", + "application: label='' icon=''", + "sdkVersion:'29'", + "maxSdkVersion:'29'", + ""); + + // WHEN parsed + ParseException e = + assertThrows(ParseException.class, () -> BadgingPackageNameParser.parse(input)); + + // THEN an IllegalArgumentException is thrown. + assertThat(e).hasMessageThat().contains("'name=' not found in package line"); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/PackagesParserTest.java b/src/test/java/com/android/tools/build/bundletool/device/PackagesParserTest.java new file mode 100644 index 00000000..0f38313b --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/PackagesParserTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PackagesParserTest { + static final ImmutableList LIST_PACKAGES_OUTPUT = + ImmutableList.of( + "package:com.android.bluetooth", + "", + "package:com.android.providers.contacts", + "package:com.android.theme.icon.roundedrect"); + + @Test + public void listPackagesOutput() { + assertThat(new PackagesParser().parse(LIST_PACKAGES_OUTPUT)) + .containsExactly( + "com.android.bluetooth", + "com.android.providers.contacts", + "com.android.theme.icon.roundedrect"); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/SdkVersionMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/SdkVersionMatcherTest.java index 61d27547..13344478 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/SdkVersionMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/SdkVersionMatcherTest.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.device; import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceWithSdk; +import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceWithSdkAndCodename; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionTargeting; import static com.google.common.truth.Truth.assertThat; @@ -120,4 +121,29 @@ public void betterAlternative() { ImmutableSet.of(sdkVersionFrom(23), SdkVersion.getDefaultInstance())))) .isFalse(); } + + @Test + public void preReleaseAppReleaseDevice_fails() { + SdkVersionMatcher sdkMatcher = new SdkVersionMatcher(deviceWithSdk(29)); + + Throwable exception; + exception = + assertThrows( + IncompatibleDeviceException.class, + () -> + sdkMatcher.checkDeviceCompatible( + sdkVersionTargeting(sdkVersionFrom(10_000), ImmutableSet.of()))); + assertThat(exception) + .hasMessageThat() + .contains("The app doesn't support SDK version of the device: (29)."); + } + + @Test + public void preReleaseAppPreReleaseDevice_succeeds() { + SdkVersionMatcher sdkMatcher = new SdkVersionMatcher(deviceWithSdkAndCodename(29, "R")); + + assertThat( + sdkMatcher.matchesTargeting(sdkVersionTargeting(sdkVersionFrom(10_000), ImmutableSet.of()))) + .isTrue(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/SessionIdParserTest.java b/src/test/java/com/android/tools/build/bundletool/device/SessionIdParserTest.java new file mode 100644 index 00000000..eddf68d5 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/SessionIdParserTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 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.device; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.exceptions.ParseException; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SessionIdParserTest { + + static final ImmutableList SESSION_CREATED_SUCCESSFULLY_OUTPUT = + ImmutableList.of("", "Success: created install session [1715550569]", ""); + + static final ImmutableList SESSION_NOT_CREATED_OUTPUT = + ImmutableList.of( + "Exception occurred while executing: Unknown option --multi-package", + "java.lang.IllegalArgumentException: Unknown option --multi-package", + "\tat com.android.server.pm.PackageManagerShellCommand.makeInstallParams(PackageManagerShellCommand.java:2757)", + "\tat com.android.server.pm.PackageManagerShellCommand.runInstallCreate(PackageManagerShellCommand.java:1292)", + "\tat com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:189)"); + + static final ImmutableList NO_SESSION_ID_OUTPUT = + ImmutableList.of("Success: created install session []"); + + private final SessionIdParser sessionIdParser = new SessionIdParser(); + + @Test + public void successfulOutput() { + assertThat(sessionIdParser.parse(SESSION_CREATED_SUCCESSFULLY_OUTPUT)).isEqualTo(1715550569); + } + + @Test + public void sessionNotCreatedOutput() { + ParseException exception = + assertThrows(ParseException.class, () -> sessionIdParser.parse(SESSION_NOT_CREATED_OUTPUT)); + assertThat(exception).hasMessageThat().contains("failed to parse session id from output"); + } + + @Test + public void noSessionIdOutput() { + ParseException exception = + assertThrows(ParseException.class, () -> sessionIdParser.parse(NO_SESSION_ID_OUTPUT)); + assertThat(exception).hasMessageThat().contains("session id should be integer"); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java b/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java index f0ac55ef..554372eb 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java @@ -655,7 +655,6 @@ public void getSize_standaloneVariant_withDimensionsAndWithoutDeviceSpec() { .setAbi("armeabi-v7a") .setScreenDensity("XHDPI") .setSdkVersion("1-20") - .setLocale("") .build(), 20L); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java b/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java index 5d5e2a2d..210d4788 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.ByteArrayInputStream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -27,7 +26,7 @@ @RunWith(JUnit4.class) public class BundleMetadataTest { - private static final InputStreamSupplier DUMMY_DATA = () -> new ByteArrayInputStream(new byte[0]); + private static final InputStreamSupplier DUMMY_DATA = InputStreamSuppliers.fromBytes(new byte[0]); @Test public void addFile_plainNamespacedDirectory() throws Exception { diff --git a/src/test/java/com/android/tools/build/bundletool/model/InputStreamSuppliersTest.java b/src/test/java/com/android/tools/build/bundletool/model/InputStreamSuppliersTest.java index 7b83772b..a99046bc 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/InputStreamSuppliersTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/InputStreamSuppliersTest.java @@ -16,7 +16,6 @@ package com.android.tools.build.bundletool.model; -import static com.android.tools.build.bundletool.testing.TestUtils.toByteArray; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.MockitoAnnotations.initMocks; @@ -52,7 +51,7 @@ public void fromFile_existingFile_ok() throws Exception { InputStreamSupplier inputStreamSupplier = InputStreamSuppliers.fromFile(filePath); - assertThat(toByteArray(inputStreamSupplier::get)).isEqualTo(fileData); + assertThat(inputStreamSupplier.asByteSource().read()).isEqualTo(fileData); } @Test @@ -90,7 +89,7 @@ public void fromFile_fileDeleted_throws() throws Exception { public void fromBytes() throws Exception { final byte[] content = {1, 34, 123}; InputStreamSupplier inputStreamSupplier = InputStreamSuppliers.fromBytes(content); - assertThat(toByteArray(inputStreamSupplier::get)).isEqualTo(content); + assertThat(inputStreamSupplier.asByteSource().read()).isEqualTo(content); } @Mock ZipFile zipFile; 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 21104026..76ab514c 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,9 +19,6 @@ import static com.android.tools.build.bundletool.testing.TestUtils.toByteArray; import static com.google.common.truth.Truth.assertThat; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.function.Supplier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -53,6 +50,22 @@ public void equals_differentPath() throws Exception { assertThat(entry1.equals(entry2)).isFalse(); } + @Test + public void equals_differentShouldCompress() throws Exception { + ModuleEntry entry1 = createEntry(ZipPath.create("a"), new byte[0]); + ModuleEntry entry2 = entry1.toBuilder().setShouldCompress(!entry1.getShouldCompress()).build(); + + assertThat(entry1.equals(entry2)).isFalse(); + } + + @Test + public void equals_differentShouldSign() throws Exception { + ModuleEntry entry1 = createEntry(ZipPath.create("a"), new byte[0]); + ModuleEntry entry2 = entry1.toBuilder().setShouldSign(!entry1.getShouldSign()).build(); + + assertThat(entry1.equals(entry2)).isFalse(); + } + @Test public void equals_differentFileContents() throws Exception { ModuleEntry entry1 = createEntry(ZipPath.create("a"), new byte[] {'a'}); @@ -69,10 +82,10 @@ public void equals_sameFiles() throws Exception { } private static ModuleEntry createEntry(ZipPath path, byte[] content) throws Exception { - return createEntry(path, () -> new ByteArrayInputStream(content)); + return createEntry(path, InputStreamSuppliers.fromBytes(content)); } - private static ModuleEntry createEntry(ZipPath path, Supplier contentSupplier) { - return ModuleEntry.builder().setPath(path).setContentSupplier(contentSupplier::get).build(); + private static ModuleEntry createEntry(ZipPath path, InputStreamSupplier contentSupplier) { + return ModuleEntry.builder().setPath(path).setContentSupplier(contentSupplier).build(); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java index f6c01604..ae9ccb50 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java @@ -32,8 +32,6 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.android.bundle.Config.ApexConfig; -import com.android.bundle.Config.ApexEmbeddedApkConfig; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -168,23 +166,11 @@ public void generateTargetingForApexImages_createsAllTargeting() throws Exceptio ApexImages apexImages = generator.generateTargetingForApexImages( - ApexConfig.newBuilder() - .addApexEmbeddedApkConfig( - ApexEmbeddedApkConfig.newBuilder() - .setPackageName("com.example") - .setPath("/Example.apk")) - .build(), allAbiFiles, /*hasBuildInfo=*/ true); List images = apexImages.getImageList(); assertThat(images).hasSize(allAbiFiles.size()); - assertThat(apexImages.getApexEmbeddedApkConfigList()) - .containsExactly( - ApexEmbeddedApkConfig.newBuilder() - .setPackageName("com.example") - .setPath("/Example.apk") - .build()); } @Test @@ -194,7 +180,6 @@ public void generateTargetingForApexImages_abiBaseNamesDisallowed() throws Excep ValidationException.class, () -> generator.generateTargetingForApexImages( - ApexConfig.getDefaultInstance(), ImmutableList.of(ZipPath.create("x86.ARM64-v8a.img")), /*hasBuildInfo=*/ false)); @@ -212,7 +197,6 @@ public void generateTargetingForApexImages_baseNameNotAnAbi_throws() throws Exce ValidationException.class, () -> generator.generateTargetingForApexImages( - ApexConfig.getDefaultInstance(), ImmutableList.of(ZipPath.create("non_abi_name.img")), /*hasBuildInfo=*/ false)); diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java index 60a87622..fca77cf3 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java @@ -17,7 +17,7 @@ package com.android.tools.build.bundletool.model.utils; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; -import static com.android.tools.build.bundletool.model.utils.ApkSizeUtils.getCompressedSizeByApkPaths; +import static com.android.tools.build.bundletool.model.utils.ApkSizeUtils.getVariantCompressedSizeByApkPaths; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksArchiveFile; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; @@ -74,7 +74,7 @@ public void oneLVariant() throws Exception { tmpDir.resolve("bundle.apks")); ImmutableMap sizeByApkPaths = - getCompressedSizeByApkPaths(variants, apksArchiveFile); + getVariantCompressedSizeByApkPaths(variants, apksArchiveFile); assertThat(sizeByApkPaths.keySet()).containsExactly("apk_one.apk"); assertThat(sizeByApkPaths.get("apk_one.apk")).isAtLeast(1L); @@ -105,7 +105,7 @@ public void oneLVariant_multipleModules() throws Exception { tmpDir.resolve("bundle.apks")); ImmutableMap sizeByApkPaths = - getCompressedSizeByApkPaths(variants, apksArchiveFile); + getVariantCompressedSizeByApkPaths(variants, apksArchiveFile); assertThat(sizeByApkPaths.keySet()) .containsExactly("base.apk", "base-x86.apk", "feature.apk", "feature-x86.apk"); @@ -136,7 +136,7 @@ public void multipleVariants() throws Exception { tmpDir.resolve("bundle.apks")); ImmutableMap sizeByApkPaths = - getCompressedSizeByApkPaths(variants, apksArchiveFile); + getVariantCompressedSizeByApkPaths(variants, apksArchiveFile); assertThat(sizeByApkPaths.keySet()).containsExactly("apk_one.apk", "apk_two.apk"); long apkOneSize = sizeByApkPaths.get("apk_one.apk"); @@ -171,7 +171,7 @@ public void multipleVariants_withUncompressedEntries() throws Exception { Path apksArchiveFile = archiveBuilder.writeTo(tmpDir.resolve("bundle.apks")); ImmutableMap sizeByApkPaths = - getCompressedSizeByApkPaths(variants, apksArchiveFile); + getVariantCompressedSizeByApkPaths(variants, apksArchiveFile); assertThat(sizeByApkPaths.keySet()).containsExactly("apk_one.apk", "apk_two.apk"); long apkOneSize = sizeByApkPaths.get("apk_one.apk"); @@ -205,7 +205,7 @@ public void multipleVariants_selectOneVariant() throws Exception { tmpDir.resolve("bundle.apks")); ImmutableMap sizeByApkPaths = - getCompressedSizeByApkPaths(ImmutableList.of(lVariant), apksArchiveFile); + getVariantCompressedSizeByApkPaths(ImmutableList.of(lVariant), apksArchiveFile); assertThat(sizeByApkPaths.keySet()).containsExactly("apk_one.apk"); assertThat(sizeByApkPaths.get("apk_one.apk")).isAtLeast(1L); diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java new file mode 100644 index 00000000..e214f48a --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2020 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.truth.Truth.assertThat; + +import com.android.tools.build.bundletool.model.ConfigurationSizes; +import com.android.tools.build.bundletool.model.SizeConfiguration; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ConfigurationSizesMergerTest { + + @Test + public void merge_defaultConfiguration() throws Exception { + ConfigurationSizes configurationSizes1 = + ConfigurationSizes.create( + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 10L), + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 20L)); + ConfigurationSizes configurationSizes2 = + ConfigurationSizes.create( + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 30L), + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 40L)); + ConfigurationSizes mergedConfigurationSizes = + ConfigurationSizesMerger.merge(configurationSizes1, configurationSizes2); + assertThat(mergedConfigurationSizes.getMinSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 40L); + assertThat(mergedConfigurationSizes.getMaxSizeConfigurationMap()) + .containsExactly(SizeConfiguration.getDefaultInstance(), 60L); + } + + @Test + public void merge_withEmptyConfiguration() throws Exception { + ConfigurationSizes configurationSizes1 = + ConfigurationSizes.create( + ImmutableMap.of(SizeConfiguration.builder().setAbi("x86").build(), 10L), + ImmutableMap.of(SizeConfiguration.builder().setAbi("x86").build(), 20L)); + ConfigurationSizes configurationSizes2 = + ConfigurationSizes.create( + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 0L), + ImmutableMap.of(SizeConfiguration.getDefaultInstance(), 0L)); + + ConfigurationSizes mergedConfigurationSizes = + ConfigurationSizesMerger.merge(configurationSizes1, configurationSizes2); + + assertThat(mergedConfigurationSizes.getMinSizeConfigurationMap()) + .isEqualTo(configurationSizes1.getMinSizeConfigurationMap()); + assertThat(mergedConfigurationSizes.getMaxSizeConfigurationMap()) + .isEqualTo(configurationSizes1.getMaxSizeConfigurationMap()); + } + + @Test + public void merge_withSubsetOfConfiguration() throws Exception { + ConfigurationSizes configurationSizes1 = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21-").build(), 10L, + SizeConfiguration.builder().setAbi("x86_64").setSdkVersion("21-").build(), 15L), + ImmutableMap.of( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21-").build(), 20L, + SizeConfiguration.builder().setAbi("x86_64").setSdkVersion("21-").build(), 25L)); + ConfigurationSizes configurationSizes2 = + ConfigurationSizes.create( + ImmutableMap.of(SizeConfiguration.builder().setSdkVersion("21-").build(), 0L), + ImmutableMap.of(SizeConfiguration.builder().setSdkVersion("21-").build(), 0L)); + + ConfigurationSizes mergedConfigurationSizes = + ConfigurationSizesMerger.merge(configurationSizes1, configurationSizes2); + + assertThat(mergedConfigurationSizes.getMinSizeConfigurationMap()) + .isEqualTo(configurationSizes1.getMinSizeConfigurationMap()); + assertThat(mergedConfigurationSizes.getMaxSizeConfigurationMap()) + .isEqualTo(configurationSizes1.getMaxSizeConfigurationMap()); + } + + @Test + public void merge_disjointDimensions() throws Exception { + final long config1X86Min = 1 << 0; + final long config1MipsMin = 1 << 1; + final long config1X86Max = 1 << 2; + final long config1MipsMax = 1 << 3; + final long config2HdpiMin = 1 << 4; + final long config2XhdpiMin = 1 << 5; + final long config2HdpiMax = 1 << 6; + final long config2XhdpiMax = 1 << 7; + ConfigurationSizes configurationSizes1 = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21-").build(), + config1X86Min, + SizeConfiguration.builder().setAbi("mips").setSdkVersion("21-").build(), + config1MipsMin), + ImmutableMap.of( + SizeConfiguration.builder().setAbi("x86").setSdkVersion("21-").build(), + config1X86Max, + SizeConfiguration.builder().setAbi("mips").setSdkVersion("21-").build(), + config1MipsMax)); + ConfigurationSizes configurationSizes2 = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setScreenDensity("hdpi").setSdkVersion("21-").build(), + config2HdpiMin, + SizeConfiguration.builder().setScreenDensity("xhdpi").setSdkVersion("21-").build(), + config2XhdpiMin), + ImmutableMap.of( + SizeConfiguration.builder().setScreenDensity("hdpi").setSdkVersion("21-").build(), + config2HdpiMax, + SizeConfiguration.builder().setScreenDensity("xhdpi").setSdkVersion("21-").build(), + config2XhdpiMax)); + + // All combinations of ABIs and screen densities should be generated. + ConfigurationSizes expectedMergedConfigurationSizes = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder() + .setAbi("x86") + .setScreenDensity("hdpi") + .setSdkVersion("21-") + .build(), + config1X86Min + config2HdpiMin, + SizeConfiguration.builder() + .setAbi("mips") + .setScreenDensity("hdpi") + .setSdkVersion("21-") + .build(), + config1MipsMin + config2HdpiMin, + SizeConfiguration.builder() + .setAbi("x86") + .setScreenDensity("xhdpi") + .setSdkVersion("21-") + .build(), + config1X86Min + config2XhdpiMin, + SizeConfiguration.builder() + .setAbi("mips") + .setScreenDensity("xhdpi") + .setSdkVersion("21-") + .build(), + config1MipsMin + config2XhdpiMin), + ImmutableMap.of( + SizeConfiguration.builder() + .setAbi("x86") + .setScreenDensity("hdpi") + .setSdkVersion("21-") + .build(), + config1X86Max + config2HdpiMax, + SizeConfiguration.builder() + .setAbi("mips") + .setScreenDensity("hdpi") + .setSdkVersion("21-") + .build(), + config1MipsMax + config2HdpiMax, + SizeConfiguration.builder() + .setAbi("x86") + .setScreenDensity("xhdpi") + .setSdkVersion("21-") + .build(), + config1X86Max + config2XhdpiMax, + SizeConfiguration.builder() + .setAbi("mips") + .setScreenDensity("xhdpi") + .setSdkVersion("21-") + .build(), + config1MipsMax + config2XhdpiMax)); + + ConfigurationSizes actualMergedConfigurationSizes = + ConfigurationSizesMerger.merge(configurationSizes1, configurationSizes2); + + assertThat(actualMergedConfigurationSizes.getMinSizeConfigurationMap()) + .isEqualTo(expectedMergedConfigurationSizes.getMinSizeConfigurationMap()); + assertThat(actualMergedConfigurationSizes.getMaxSizeConfigurationMap()) + .isEqualTo(expectedMergedConfigurationSizes.getMaxSizeConfigurationMap()); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocatorTest.java index 0b21802a..ee9f815d 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/SdkToolsLocatorTest.java @@ -126,9 +126,30 @@ public static final class AdbOnSystemPathTest { public static Collection data() { return Arrays.asList( new Object[][] { - {"Unix", Configuration.unix(), ":", "/nonexistent:/bin", "/bin", ""}, - {"Windows", Configuration.windows(), ";", "C:\\nonexistent;C:\\bin", "C:\\bin", ".exe"}, - {"OS X", Configuration.osX(), ":", "/nonexistent:/bin", "/bin", ""} + { + /* osName */ "Unix", + /* osConfiguration */ Configuration.unix(), + /* pathSeparator */ ":", + /* systemPath */ "/nonexistent:/bin:\"invalid-path", + /* goodPathDir */ "/bin", + /* executableExtension */ "" + }, + { + /* osName */ "Windows", + /* osConfiguration */ Configuration.windows(), + /* pathSeparator */ ";", + /* systemPath */ "C:\\nonexistent;C:\\bin;\"invalid-path", + /* goodPathDir */ "C:\\bin", + /* executableExtension */ ".exe" + }, + { + /* osName */ "OS X", + /* osConfiguration */ Configuration.osX(), + /* pathSeparator */ ":", + /* systemPath */ "/nonexistent:/bin:\"invalid-path", + /* goodPathDir */ "/bin", + /* executableExtension */ "" + } }); } diff --git a/src/test/java/com/android/tools/build/bundletool/size/ApkBreakdownGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/size/ApkBreakdownGeneratorTest.java index 237dfec3..2868afab 100644 --- a/src/test/java/com/android/tools/build/bundletool/size/ApkBreakdownGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/size/ApkBreakdownGeneratorTest.java @@ -16,7 +16,9 @@ package com.android.tools.build.bundletool.size; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import com.android.bundle.SizesOuterClass.Breakdown; import com.android.bundle.SizesOuterClass.Sizes; @@ -25,9 +27,12 @@ import com.android.tools.build.bundletool.model.ZipPath; import com.google.auto.value.AutoValue; import com.google.common.io.ByteStreams; +import com.google.common.primitives.Bytes; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.zip.Deflater; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -42,8 +47,6 @@ @RunWith(JUnit4.class) public class ApkBreakdownGeneratorTest { - private static final byte[] BYTES = new byte[256]; - @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Path tmpDir; @@ -55,28 +58,33 @@ public void setUp() throws Exception { @Test public void computesBreakdown_resources() throws Exception { + byte[] resources = "I am a resouce table for an android app".getBytes(UTF_8); + ZipEntryInfo entry = ZipEntryInfo.builder() .setName("resources.arsc") - .setContent(BYTES) + .setContent(resources) .setCompress(false) .build(); Path archive = createZipArchiveWith(entry); long archiveSize = Files.size(archive); long downloadedArchiveSize = gzipOverArchive(Files.readAllBytes(archive)).length; + long resourcesDownloadSize = compress(resources); Breakdown breakdown = ApkBreakdownGenerator.calculateBreakdown(archive); assertThat(breakdown) .ignoringRepeatedFieldOrder() .isEqualTo( - emptyBreakdownProto() - .toBuilder() - .setResources(Sizes.newBuilder().setDiskSize(256).setDownloadSize(10)) + emptyBreakdownProto().toBuilder() + .setResources( + Sizes.newBuilder() + .setDiskSize(resources.length) + .setDownloadSize(resourcesDownloadSize)) .setOther( Sizes.newBuilder() - .setDiskSize(archiveSize - 256) - .setDownloadSize(downloadedArchiveSize - 10)) + .setDiskSize(archiveSize - resources.length) + .setDownloadSize(downloadedArchiveSize - resourcesDownloadSize)) .setTotal( Sizes.newBuilder() .setDiskSize(archiveSize) @@ -86,36 +94,50 @@ public void computesBreakdown_resources() throws Exception { @Test public void computesBreakdown_resourcesMultiple() throws Exception { + byte[] resourceTable = "I am a resouce table for an android app".getBytes(UTF_8); + byte[] aResource = "I am a resource in an apk file".getBytes(UTF_8); ZipEntryInfo compressedEntry = ZipEntryInfo.builder() .setName("res/raw/song01.ogg") - .setContent(BYTES) + .setContent(aResource) .setCompress(true) .build(); - ZipEntryInfo resourceTable = + ZipEntryInfo resourceTableEntry = ZipEntryInfo.builder() .setName("resources.arsc") - .setContent(BYTES) + .setContent(resourceTable) .setCompress(false) .build(); - long compressedEntrySize = getCompressedSize(compressedEntry); - Path archive = createZipArchiveWith(resourceTable, compressedEntry); + Path archive = createZipArchiveWith(resourceTableEntry, compressedEntry); long archiveSize = Files.size(archive); + + // Because of the way we compress each entry with a flush between each compression we can't + // calculate this without just repeating the prod code. + // However we do know that it should be larger than compressing both entries in one batch + // but smaller than compressing the entries independently. + long resourcesDownloadSize = 57; + assertThat(resourcesDownloadSize) + .isGreaterThan(compress(Bytes.concat(resourceTable, aResource))); + assertThat(resourcesDownloadSize).isLessThan(compress(resourceTable) + compress(aResource)); + + long resourcesDiskSize = + getCompressedSize(compressedEntry) + getCompressedSize(resourceTableEntry); long downloadedArchiveSize = gzipOverArchive(Files.readAllBytes(archive)).length; Breakdown breakdown = ApkBreakdownGenerator.calculateBreakdown(archive); assertThat(breakdown) .ignoringRepeatedFieldOrder() .isEqualTo( - emptyBreakdownProto() - .toBuilder() + emptyBreakdownProto().toBuilder() .setResources( - Sizes.newBuilder().setDiskSize(256 + compressedEntrySize).setDownloadSize(18)) + Sizes.newBuilder() + .setDiskSize(resourcesDiskSize) + .setDownloadSize(resourcesDownloadSize)) // Expecting the zip/gzip overheads to be accounted for in OTHER. .setOther( Sizes.newBuilder() - .setDiskSize(archiveSize - (256 + compressedEntrySize)) - .setDownloadSize(downloadedArchiveSize - 18)) + .setDiskSize(archiveSize - resourcesDiskSize) + .setDownloadSize(downloadedArchiveSize - resourcesDownloadSize)) .setTotal( Sizes.newBuilder() .setDiskSize(archiveSize) @@ -125,23 +147,25 @@ public void computesBreakdown_resourcesMultiple() throws Exception { @Test public void computesBreakdown_dex() throws Exception { + byte[] dex = "this is the contents of a dex file".getBytes(UTF_8); + ZipEntryInfo dexEntry = - ZipEntryInfo.builder().setName("classes.dex").setContent(BYTES).setCompress(false).build(); + ZipEntryInfo.builder().setName("classes.dex").setContent(dex).setCompress(false).build(); Path archive = createZipArchiveWith(dexEntry); long archiveSize = Files.size(archive); long downloadedArchiveSize = gzipOverArchive(Files.readAllBytes(archive)).length; + long dexDownloadSize = compress(dex); Breakdown breakdown = ApkBreakdownGenerator.calculateBreakdown(archive); assertThat(breakdown) .ignoringRepeatedFieldOrder() .isEqualTo( - emptyBreakdownProto() - .toBuilder() - .setDex(Sizes.newBuilder().setDiskSize(256).setDownloadSize(10)) + emptyBreakdownProto().toBuilder() + .setDex(Sizes.newBuilder().setDiskSize(dex.length).setDownloadSize(dexDownloadSize)) .setOther( Sizes.newBuilder() - .setDiskSize(archiveSize - 256) - .setDownloadSize(downloadedArchiveSize - 10)) + .setDiskSize(archiveSize - dex.length) + .setDownloadSize(downloadedArchiveSize - dexDownloadSize)) .setTotal( Sizes.newBuilder() .setDiskSize(archiveSize) @@ -151,28 +175,32 @@ public void computesBreakdown_dex() throws Exception { @Test public void computesBreakdown_assets() throws Exception { + byte[] assets = "this is a game asset".getBytes(UTF_8); ZipEntryInfo assetsEntry = ZipEntryInfo.builder() .setName("assets/intro.mp4") - .setContent(BYTES) + .setContent(assets) .setCompress(false) .build(); Path archive = createZipArchiveWith(assetsEntry); long archiveSize = Files.size(archive); long downloadedArchiveSize = gzipOverArchive(Files.readAllBytes(archive)).length; + long assetsDownloadSize = compress(assets); Breakdown breakdown = ApkBreakdownGenerator.calculateBreakdown(archive); assertThat(breakdown) .ignoringRepeatedFieldOrder() .isEqualTo( - emptyBreakdownProto() - .toBuilder() - .setAssets(Sizes.newBuilder().setDiskSize(256).setDownloadSize(10)) + emptyBreakdownProto().toBuilder() + .setAssets( + Sizes.newBuilder() + .setDiskSize(assets.length) + .setDownloadSize(assetsDownloadSize)) // Expecting the zip/gzip overheads to be accounted for in OTHER. .setOther( Sizes.newBuilder() - .setDiskSize(archiveSize - 256) - .setDownloadSize(downloadedArchiveSize - 10)) + .setDiskSize(archiveSize - assets.length) + .setDownloadSize(downloadedArchiveSize - assetsDownloadSize)) .setTotal( Sizes.newBuilder() .setDiskSize(archiveSize) @@ -182,27 +210,30 @@ public void computesBreakdown_assets() throws Exception { @Test public void computesBreakdown_nativeLibs() throws Exception { + byte[] nativeLib = "this is a native lib".getBytes(UTF_8); ZipEntryInfo nativeLibEntry = ZipEntryInfo.builder() .setName("lib/arm64-v8a/libcrashalytics.so") - .setContent(BYTES) + .setContent(nativeLib) .setCompress(false) .build(); Path archive = createZipArchiveWith(nativeLibEntry); long archiveSize = Files.size(archive); long downloadedArchiveSize = gzipOverArchive(Files.readAllBytes(archive)).length; - + long nativeLibDownloadSize = compress(nativeLib); Breakdown breakdown = ApkBreakdownGenerator.calculateBreakdown(archive); assertThat(breakdown) .ignoringRepeatedFieldOrder() .isEqualTo( - emptyBreakdownProto() - .toBuilder() - .setNativeLibs(Sizes.newBuilder().setDiskSize(256).setDownloadSize(10)) + emptyBreakdownProto().toBuilder() + .setNativeLibs( + Sizes.newBuilder() + .setDiskSize(nativeLib.length) + .setDownloadSize(nativeLibDownloadSize)) .setOther( Sizes.newBuilder() - .setDiskSize(archiveSize - 256) - .setDownloadSize(downloadedArchiveSize - 10)) + .setDiskSize(archiveSize - nativeLib.length) + .setDownloadSize(downloadedArchiveSize - nativeLibDownloadSize)) .setTotal( Sizes.newBuilder() .setDiskSize(archiveSize) @@ -212,10 +243,11 @@ public void computesBreakdown_nativeLibs() throws Exception { @Test public void computesBreakdown_other() throws Exception { + byte[] other = "this is a random datafile".getBytes(UTF_8); ZipEntryInfo otherEntry = ZipEntryInfo.builder() .setName("org/hamcrest/something.cfg") - .setContent(BYTES) + .setContent(other) .setCompress(false) .build(); Path archive = createZipArchiveWith(otherEntry); @@ -239,6 +271,14 @@ public void computesBreakdown_other() throws Exception { .build()); } + @Test + public void checkDeflaterSyncOverheadCorrect() throws Exception { + Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, /* noWrap */ true); + byte[] output = new byte[100]; + assertThat(deflater.deflate(output, 0, output.length, Deflater.SYNC_FLUSH)) + .isEqualTo(ApkBreakdownGenerator.DEFLATER_SYNC_OVERHEAD_BYTES); + } + private static byte[] gzipOverArchive(byte[] archive) throws Exception { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) { @@ -251,11 +291,15 @@ private static long getCompressedSize(ZipEntryInfo entry) throws Exception { if (!entry.getCompress()) { return entry.getContent().length; } - ZipEntry zipEntry = new ZipEntry(entry.getName()); + return compress(entry.getContent()); + } + + private static long compress(byte[] data) throws IOException { + ZipEntry zipEntry = new ZipEntry("entry"); try (ZipOutputStream zos = new ZipOutputStream(ByteStreams.nullOutputStream())) { zipEntry.setMethod(ZipEntry.DEFLATED); zos.putNextEntry(zipEntry); - zos.write(entry.getContent()); + zos.write(data); zos.closeEntry(); } return zipEntry.getCompressedSize(); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java index a1ca8a73..7e864f70 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java @@ -59,8 +59,11 @@ import static junit.framework.TestCase.fail; import com.android.aapt.ConfigurationOuterClass.Configuration; +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Item; import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.StringPool; +import com.android.aapt.Resources.Value; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ScreenDensity; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; @@ -79,7 +82,9 @@ import com.google.common.collect.Sets; import com.google.common.truth.Truth8; import com.google.protobuf.ByteString; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import org.junit.Test; @@ -101,7 +106,8 @@ public class ScreenDensityResourcesSplitterTest { new ScreenDensityResourcesSplitter( BundleToolVersion.getCurrentVersion(), NO_RESOURCES_PINNED_TO_MASTER, - NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER); + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); @DataPoints("bundleFeatureEnabled") public static final ImmutableSet BUNDLE_FEATURE_ENABLED_DATA_POINTS = @@ -511,7 +517,8 @@ public void densityBucket_neighbouringResources_edgeCase() throws Exception { ImmutableSet.of(DensityAlias.XXHDPI, DensityAlias.XXXHDPI), BundleToolVersion.getCurrentVersion(), NO_RESOURCES_PINNED_TO_MASTER, - NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER); + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster =*/ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(module)); @@ -703,7 +710,8 @@ public void defaultDensityWithAlternatives_before_0_4_0() throws Exception { new ScreenDensityResourcesSplitter( Version.of("0.3.3"), NO_RESOURCES_PINNED_TO_MASTER, - NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER); + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection splits = splitter.split(ModuleSplit.forResources(module)); // Master split: Resource present with default targeting. @@ -787,7 +795,8 @@ public void nonDefaultDensityResourceWithoutAlternatives_inMasterSince_0_4_0( new ScreenDensityResourcesSplitter( Version.of(bundleFeatureEnabled ? "0.4.0" : "0.3.3"), NO_RESOURCES_PINNED_TO_MASTER, - NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER); + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection splits = splitter.split(ModuleSplit.forResources(module)); if (bundleFeatureEnabled) { @@ -917,7 +926,8 @@ public void resourcesPinnedToMaster_splittingSupressed() throws Exception { new ScreenDensityResourcesSplitter( BundleToolVersion.getCurrentVersion(), masterResourcesPredicate, - NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER); + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(testModule)); @@ -968,7 +978,8 @@ public void lowestDensityConfigsPinnedToMaster() throws Exception { new ScreenDensityResourcesSplitter( BundleToolVersion.getCurrentVersion(), NO_RESOURCES_PINNED_TO_MASTER, - pinnedLowDensityResourcesPredicate); + pinnedLowDensityResourcesPredicate, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(testModule)); @@ -987,6 +998,78 @@ public void lowestDensityConfigsPinnedToMaster() throws Exception { } } + @Test + public void lowestDensityStylesPinnedToMaster_enabled() throws Exception { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .setResourceTable( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addResource( + "style", + "title_text_size", + configValueWithDensity("320dens", Optional.of(XHDPI)), + configValueWithDensity("default", Optional.empty())) + .build()) + .setManifest(androidManifest("com.test.app")) + .build(); + + ScreenDensityResourcesSplitter splitter = + new ScreenDensityResourcesSplitter( + BundleToolVersion.getCurrentVersion(), + NO_RESOURCES_PINNED_TO_MASTER, + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ true); + + ImmutableCollection splits = splitter.split(ModuleSplit.forResources(testModule)); + + ModuleSplit masterSplit = + splits.stream().filter(ModuleSplit::isMasterSplit).collect(onlyElement()); + assertThat(extractStyleValues(masterSplit, "title_text_size")).containsExactly("default"); + + ModuleSplit xhdpiSplit = extractDensityTargetingModule(splits, DensityAlias.XXHDPI).get(); + assertThat(extractStyleValues(xhdpiSplit, "title_text_size")).containsExactly("320dens"); + + Optional mdpiSplit = extractDensityTargetingModule(splits, DensityAlias.MDPI); + assertThat(mdpiSplit).isEmpty(); + } + + @Test + public void lowestDensityStylesPinnedToMaster_disabled() throws Exception { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .setResourceTable( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addResource( + "style", + "title_text_size", + configValueWithDensity("320dens", Optional.of(XHDPI)), + configValueWithDensity("default", Optional.empty())) + .build()) + .setManifest(androidManifest("com.test.app")) + .build(); + + ScreenDensityResourcesSplitter splitter = + new ScreenDensityResourcesSplitter( + BundleToolVersion.getCurrentVersion(), + NO_RESOURCES_PINNED_TO_MASTER, + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); + + ImmutableCollection splits = splitter.split(ModuleSplit.forResources(testModule)); + + ModuleSplit masterSplit = + splits.stream().filter(ModuleSplit::isMasterSplit).collect(onlyElement()); + assertThat(extractStyleValues(masterSplit, "title_text_size")).isEmpty(); + + ModuleSplit xhdpiSplit = extractDensityTargetingModule(splits, DensityAlias.XXHDPI).get(); + assertThat(extractStyleValues(xhdpiSplit, "title_text_size")).containsExactly("320dens"); + + ModuleSplit mdpiSplit = extractDensityTargetingModule(splits, DensityAlias.MDPI).get(); + assertThat(extractStyleValues(mdpiSplit, "title_text_size")).containsExactly("default"); + } + @Test public void lowestDensityConfigsPinnedToMaster_mixedConfigsInSameDensityBucket() throws Exception { @@ -1023,7 +1106,8 @@ public void lowestDensityConfigsPinnedToMaster_mixedConfigsInSameDensityBucket() new ScreenDensityResourcesSplitter( BundleToolVersion.getCurrentVersion(), NO_RESOURCES_PINNED_TO_MASTER, - pinnedLowDensityResourcesPredicate); + pinnedLowDensityResourcesPredicate, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(testModule)); @@ -1092,7 +1176,8 @@ public void lowestDensityConfigsPinnedToMaster_masterCoversRangeOfDensities() th new ScreenDensityResourcesSplitter( BundleToolVersion.getCurrentVersion(), NO_RESOURCES_PINNED_TO_MASTER, - pinnedLowDensityResourcesPredicate); + pinnedLowDensityResourcesPredicate, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(testModule)); @@ -1119,6 +1204,40 @@ public void lowestDensityConfigsPinnedToMaster_masterCoversRangeOfDensities() th } } + private static ConfigValue configValueWithDensity(String value, Optional density) { + return ConfigValue.newBuilder() + .setConfig(density.orElse(Configuration.getDefaultInstance())) + .setValue( + Value.newBuilder() + .setItem( + Item.newBuilder() + .setStr(com.android.aapt.Resources.String.newBuilder().setValue(value)))) + .build(); + } + + private static ImmutableList extractStyleValues( + ModuleSplit moduleSplit, String styleName) { + return moduleSplit.getResourceTable().get().getPackageList().stream() + .flatMap(pack -> pack.getTypeList().stream()) + .filter(type -> "style".equals(type.getName())) + .flatMap(type -> type.getEntryList().stream()) + .filter(entry -> styleName.equals(entry.getName())) + .flatMap(entry -> entry.getConfigValueList().stream()) + .map(value -> value.getValue().getItem().getStr().getValue()) + .collect(toImmutableList()); + } + + private static Optional extractDensityTargetingModule( + Collection modules, DensityAlias targeting) { + return modules.stream() + .filter( + moduleSplit -> + moduleSplit.getApkTargeting().getScreenDensityTargeting().getValueList().stream() + .map(ScreenDensity::getDensityAlias) + .anyMatch(targeting::equals)) + .findFirst(); + } + private static ModuleSplit findModuleSplitWithScreenDensityTargeting( ImmutableCollection moduleSplits, DensityAlias densityAlias) { return findModuleSplitWithScreenDensityTargeting( 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 2c91086d..b3935556 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,6 +26,8 @@ import com.android.bundle.Commands.ApexApkMetadata; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; +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.ModuleMetadata; @@ -261,7 +263,16 @@ public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, ZipPath apkPa .build(); } - private static Stream apkDescriptionStream(BuildApksResult buildApksResult) { + public static AssetSliceSet createAssetSliceSet( + String moduleName, DeliveryType deliveryType, ApkDescription... apkDescriptions) { + return AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder().setName(moduleName).setDeliveryType(deliveryType)) + .addAllApkDescription(Arrays.asList(apkDescriptions)) + .build(); + } + + public static Stream apkDescriptionStream(BuildApksResult buildApksResult) { return Stream.concat( buildApksResult.getVariantList().stream() .flatMap(variant -> variant.getApkSetList().stream()) @@ -269,4 +280,6 @@ private static Stream apkDescriptionStream(BuildApksResult build buildApksResult.getAssetSliceSetList().stream() .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream())); } + + private ApksArchiveHelpers() {} } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java index b2772032..08f2edd7 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java @@ -20,6 +20,7 @@ import com.android.bundle.Config.Bundletool; import com.android.bundle.Config.SplitDimension; import com.android.bundle.Config.SuffixStripping; +import com.android.bundle.Config.UnsignedEmbeddedApkConfig; import com.android.tools.build.bundletool.model.version.BundleToolVersion; /** Helper to create {@link BundleConfig} instances in tests. */ @@ -108,6 +109,11 @@ public BundleConfigBuilder addStandaloneDimension(SplitDimension standaloneDimen return this; } + public BundleConfigBuilder addUnsignedEmbeddedApkConfig(String path) { + builder.addUnsignedEmbeddedApkConfig(UnsignedEmbeddedApkConfig.newBuilder().setPath(path)); + return this; + } + public BundleConfigBuilder clearCompression() { builder.clearCompression(); return this; diff --git a/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java b/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java index 30edfc28..82472b25 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java @@ -31,9 +31,24 @@ /** Factory to create {@link DeviceSpec} instances. */ public final class DeviceFactory { + public static DeviceSpec deviceWithSdkAndCodename(int sdkVersion, String codename) { + return mergeSpecs( + sdkVersion(sdkVersion, codename), + abis("arm64-v8a"), + density(DensityAlias.MDPI), + locales("en-US")); + } + public static DeviceSpec deviceWithSdk(int sdkVersion) { + return deviceWithSdkAndCodename(sdkVersion, "REL"); + } + + public static DeviceSpec qDeviceWithLocales(String... locales) { return mergeSpecs( - sdkVersion(sdkVersion), abis("arm64-v8a"), density(DensityAlias.MDPI), locales("en-US")); + sdkVersion(Versions.ANDROID_Q_API_VERSION), + abis("arm64-v8a"), + density(DensityAlias.MDPI), + locales(locales)); } public static DeviceSpec lDeviceWithLocales(String... locales) { @@ -106,6 +121,10 @@ public static DeviceSpec sdkVersion(int sdkVersion) { return DeviceSpec.newBuilder().setSdkVersion(sdkVersion).build(); } + public static DeviceSpec sdkVersion(int sdkVersion, String codename) { + return DeviceSpec.newBuilder().setSdkVersion(sdkVersion).setCodename(codename).build(); + } + public static DeviceSpec deviceFeatures(String... features) { return DeviceSpec.newBuilder().addAllDeviceFeatures(Arrays.asList(features)).build(); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java index 1e3ee95f..8efaa195 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -221,7 +222,10 @@ public void executeShellCommand( throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { - checkState(commandInjections.containsKey(command)); + checkState( + commandInjections.containsKey(command), + "Command %s not found in command injections.", + command); byte[] data = commandInjections.get(command).onExecute().getBytes(UTF_8); receiver.addOutput(data, 0, data.length); receiver.flush(); @@ -243,6 +247,15 @@ public void pushApks(ImmutableList apks, PushOptions pushOptions) { pushApksSideEffect.ifPresent(val -> val.apply(apks, pushOptions)); } + @Override + public Path syncPackageToDevice(Path localFilePath) { + checkState(Files.exists(localFilePath)); + return Paths.get("/temp", localFilePath.getFileName().toString()); + } + + @Override + public void removeRemotePackage(Path remoteFilePath) {} + public void setInstallApksSideEffect(SideEffect sideEffect) { installApksSideEffect = Optional.of(sideEffect); } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java index 99fb9d88..b7462137 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java @@ -92,7 +92,14 @@ public void validateDexFile_badDirectoryStructure_throws() throws Exception { @Test public void validateLibFile_valid_success() throws Exception { - ZipPath libFile = ZipPath.create("lib/x86/libX.so"); + ZipPath libFile = ZipPath.create("lib/x86/foo.so"); + + new BundleFilesValidator().validateModuleFile(libFile); + } + + @Test + public void validateWrapSh_valid_success() throws Exception { + ZipPath libFile = ZipPath.create("lib/x86/wrap.sh"); new BundleFilesValidator().validateModuleFile(libFile); }