From 8f922cf4b24bb1714d06cfa303d8e79e81b71a91 Mon Sep 17 00:00:00 2001 From: Pierre Lecesne Date: Fri, 20 Dec 2019 11:44:14 +0000 Subject: [PATCH] Prepare for release 0.12.0. --- README.md | 2 +- build.gradle | 5 + gradle.properties | 2 +- .../bundletool/commands/BuildApksManager.java | 8 +- .../commands/InstallApksCommand.java | 118 ++++++- .../{ApksInstaller.java => AdbRunner.java} | 32 +- .../build/bundletool/device/ApkMatcher.java | 3 +- .../build/bundletool/device/DdmlibDevice.java | 200 ++++++++++- .../tools/build/bundletool/device/Device.java | 44 ++- .../bundletool/io/ApkSerializerHelper.java | 21 +- .../bundletool/io/ApkSerializerManager.java | 1 + .../mergers/ModuleSplitsToShardMerger.java | 101 +++++- .../bundletool/model/AndroidManifest.java | 31 +- .../build/bundletool/model/AppBundle.java | 3 +- .../build/bundletool/model/BundleModule.java | 6 +- .../build/bundletool/model/ModuleSplit.java | 33 +- .../tools/build/bundletool/model/ZipPath.java | 118 +------ .../targeting/ScreenDensitySelector.java | 3 +- .../model/targeting/TargetedDirectory.java | 5 +- .../targeting/TargetedDirectorySegment.java | 17 +- .../model/targeting/TargetingUtils.java | 52 ++- .../bundletool/model/utils/PathMatcher.java | 184 ++++++++++ .../bundletool/model/utils/ResultUtils.java | 16 + .../model/utils/TargetingNormalizer.java | 210 +++++++++++ .../model/utils/TargetingProtoUtils.java | 11 +- .../model/utils/TextureCompressionUtils.java | 14 +- .../model/utils/files/FilePreconditions.java | 17 +- .../model/utils/files/FileUtils.java | 7 +- .../model/version/BundleToolVersion.java | 4 +- .../model/version/VersionGuardedFeature.java | 53 ++- .../AppBundleObfuscationPreprocessor.java | 6 +- .../splitters/AssetModuleSplitter.java | 9 +- .../splitters/AssetSlicesGenerator.java | 10 +- .../AssetsDimensionSplitterFactory.java | 66 +++- .../bundletool/splitters/BundleSharder.java | 20 +- .../splitters/ResourceAnalyzer.java | 7 + .../ScreenDensityResourcesSplitter.java | 16 +- .../validation/AbiParityValidator.java | 2 +- .../validation/AndroidManifestValidator.java | 2 + .../validation/ApexBundleValidator.java | 48 ++- .../validation/BundleConfigValidator.java | 25 +- .../validation/ModuleTitleValidator.java | 8 +- src/main/proto/apex_manifest.proto | 34 ++ src/main/proto/commands.proto | 3 + src/main/proto/config.proto | 5 + .../commands/BuildApksCommandTest.java | 28 ++ .../commands/BuildApksManagerTest.java | 258 +++++++++----- .../commands/ExtractApksCommandTest.java | 18 +- .../commands/InstallApksCommandTest.java | 50 +++ ...sInstallerTest.java => AdbRunnerTest.java} | 60 ++-- .../bundletool/device/DdmlibDeviceTest.java | 36 ++ .../VariantTotalSizeAggregatorTest.java | 3 +- .../ModuleSplitsToShardMergerTest.java | 63 ++-- .../bundletool/model/AndroidManifestTest.java | 13 +- .../model/FileSystemModuleEntryTest.java | 1 - .../bundletool/model/ModuleSplitTest.java | 2 +- .../build/bundletool/model/ZipPathTest.java | 16 +- ...ernativeVariantTargetingPopulatorTest.java | 16 +- .../TargetedDirectorySegmentTest.java | 117 +++---- .../model/utils/PathMatcherTest.java | 327 ++++++++++++++++++ .../model/utils/ResultUtilsTest.java | 51 ++- .../model/utils/TargetingNormalizerTest.java | 109 ++++++ .../model/utils/ThrowableUtilsTest.java | 6 +- .../bundletool/model/utils/ZipUtilsTest.java | 2 +- .../model/utils/files/FileUtilsTest.java | 17 +- .../splitters/AbiApexImagesSplitterTest.java | 8 +- .../splitters/AssetModuleSplitterTest.java | 5 +- .../splitters/AssetSlicesGeneratorTest.java | 12 +- .../splitters/AssetsLanguageSplitterTest.java | 13 +- .../splitters/BundleSharderTest.java | 43 +-- .../GraphicsApiAssetsSplitterTest.java | 32 +- .../splitters/ShardedApksGeneratorTest.java | 30 +- ...reCompressionFormatAssetsSplitterTest.java | 8 +- .../testing/ApksArchiveHelpers.java | 8 +- .../build/bundletool/testing/FakeDevice.java | 18 +- .../build/bundletool/testing/ProtoFuzzer.java | 133 +++++++ .../bundletool/testing/ProtoFuzzerTest.java | 98 ++++++ .../bundletool/testing/ProtoReflection.java | 82 +++++ .../testing/ProtoReflectionTest.java | 67 ++++ .../build/bundletool/testing/TestUtils.java | 11 +- .../validation/ApexBundleValidatorTest.java | 9 +- .../validation/BundleConfigValidatorTest.java | 24 +- 82 files changed, 2697 insertions(+), 679 deletions(-) rename src/main/java/com/android/tools/build/bundletool/device/{ApksInstaller.java => AdbRunner.java} (70%) create mode 100755 src/main/java/com/android/tools/build/bundletool/model/utils/PathMatcher.java create mode 100755 src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java create mode 100755 src/main/proto/apex_manifest.proto rename src/test/java/com/android/tools/build/bundletool/device/{ApksInstallerTest.java => AdbRunnerTest.java} (76%) create mode 100755 src/test/java/com/android/tools/build/bundletool/model/utils/PathMatcherTest.java create mode 100755 src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java create mode 100755 src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzer.java create mode 100755 src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzerTest.java create mode 100755 src/test/java/com/android/tools/build/bundletool/testing/ProtoReflection.java create mode 100755 src/test/java/com/android/tools/build/bundletool/testing/ProtoReflectionTest.java diff --git a/README.md b/README.md index fee36f5e..6f171528 100755 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [0.11.0](https://github.com/google/bundletool/releases) +Latest release: [0.12.0](https://github.com/google/bundletool/releases) diff --git a/build.gradle b/build.gradle index c2a00e4b..cbece782 100755 --- a/build.gradle +++ b/build.gradle @@ -143,6 +143,11 @@ shadowJar { exclude 'com.android.bundle.**' // Aapt protos. exclude 'com.android.aapt.**' + // String constants in classes. + // For some reason, the Shadow plug-in seems to rename strings in classes too! + exclude 'com.android.vending.splits' + exclude 'com.android.vending.splits.required' + exclude 'com.android.dynamic.apk.fused.modules' } } diff --git a/gradle.properties b/gradle.properties index dba9418b..a5ec9887 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 0.11.0 +release_version = 0.12.0 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 bae38de2..ad203dfb 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -19,6 +19,7 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndExecutable; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -178,10 +179,7 @@ private void executeWithZip(ZipFile bundleZip, Optional deviceSpec) ? modulesToFuse(appBundle.getFeatureModules().values().asList()) : requestedModules.asList(); generatedApksBuilder.setStandaloneApks( - new ShardedApksGenerator( - tempDir, - bundleVersion, - getSuffixStrippings(bundleConfig)) + new ShardedApksGenerator(tempDir, bundleVersion, getSuffixStrippings(bundleConfig)) .generateSplits( modulesToFuse, appBundle.getBundleMetadata(), @@ -286,7 +284,7 @@ private ImmutableList generateSplitApks(AppBundle appBundle) throws Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion()); ApkGenerationConfiguration.Builder apkGenerationConfiguration = getCommonSplitApkGenerationConfiguration(appBundle); - if (!bundleVersion.isOlderThan(Version.of("0.8.1"))) { + if (RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT.enabledForVersion(bundleVersion)) { // Make sure that resources reachable from the manifest of the base module will be // represented in the master split (by at least one config). This prevents the app // from crashing too soon (before reaching Application#onCreate), in case when only 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 b043d3fb..92b25907 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -21,12 +21,15 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkDirectoryExists; 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.google.common.base.Preconditions.checkArgument; +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.AdbRunner; import com.android.tools.build.bundletool.device.AdbServer; -import com.android.tools.build.bundletool.device.ApksInstaller; +import com.android.tools.build.bundletool.device.Device; import com.android.tools.build.bundletool.device.Device.InstallOptions; import com.android.tools.build.bundletool.device.DeviceAnalyzer; import com.android.tools.build.bundletool.flags.Flag; @@ -34,6 +37,7 @@ import com.android.tools.build.bundletool.io.TempDirectory; 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; @@ -54,6 +58,9 @@ public abstract class InstallApksCommand { private static final Flag DEVICE_ID_FLAG = Flag.string("device-id"); private static final Flag> MODULES_FLAG = Flag.stringSet("modules"); private static final Flag ALLOW_DOWNGRADE_FLAG = Flag.booleanFlag("allow-downgrade"); + private static final Flag PUSH_SPLITS_FLAG = Flag.string("push-splits-to"); + private static final Flag CLEAR_PUSH_PATH_FLAG = Flag.booleanFlag("clear-push-path"); + private static final Flag ALLOW_TEST_ONLY_FLAG = Flag.booleanFlag("allow-test-only"); private static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; @@ -70,10 +77,19 @@ public abstract class InstallApksCommand { public abstract boolean getAllowDowngrade(); + public abstract Optional getPushSplitsPath(); + + public abstract boolean getClearPushPath(); + + public abstract boolean getAllowTestOnly(); + abstract AdbServer getAdbServer(); public static Builder builder() { - return new AutoValue_InstallApksCommand.Builder().setAllowDowngrade(false); + return new AutoValue_InstallApksCommand.Builder() + .setAllowDowngrade(false) + .setClearPushPath(false) + .setAllowTestOnly(false); } /** Builder for the {@link InstallApksCommand}. */ @@ -92,6 +108,12 @@ public abstract static class Builder { /** The caller is responsible for the lifecycle of the {@link AdbServer}. */ public abstract Builder setAdbServer(AdbServer adbServer); + public abstract Builder setPushSplitsPath(String pushSplitsPath); + + public abstract Builder setClearPushPath(boolean clearPushPath); + + public abstract Builder setAllowTestOnly(boolean allowTestOnly); + public abstract InstallApksCommand build(); } @@ -123,6 +145,9 @@ public static InstallApksCommand fromFlags( Optional> modules = MODULES_FLAG.getValue(flags); Optional allowDowngrade = ALLOW_DOWNGRADE_FLAG.getValue(flags); + Optional pushSplits = PUSH_SPLITS_FLAG.getValue(flags); + Optional clearPushPath = CLEAR_PUSH_PATH_FLAG.getValue(flags); + Optional allowTestOnly = ALLOW_TEST_ONLY_FLAG.getValue(flags); flags.checkNoUnknownFlags(); @@ -131,6 +156,7 @@ public static InstallApksCommand fromFlags( deviceSerialName.ifPresent(command::setDeviceId); modules.ifPresent(command::setModules); allowDowngrade.ifPresent(command::setAllowDowngrade); + allowTestOnly.ifPresent(command::setAllowTestOnly); return command.build(); } @@ -153,17 +179,73 @@ public void execute() { extractApksCommand.setOutputDirectory(tempDirectory.getPath()); } getModules().ifPresent(extractApksCommand::setModules); - ImmutableList extractedApks = extractApksCommand.build().execute(); + final ImmutableList extractedApks = extractApksCommand.build().execute(); - ApksInstaller installer = new ApksInstaller(adbServer); + AdbRunner adbRunner = new AdbRunner(adbServer); InstallOptions installOptions = - InstallOptions.builder().setAllowDowngrade(getAllowDowngrade()).build(); + InstallOptions.builder() + .setAllowDowngrade(getAllowDowngrade()) + .setAllowTestOnly(getAllowTestOnly()) + .build(); if (getDeviceId().isPresent()) { - installer.installApks(extractedApks, installOptions, getDeviceId().get()); + adbRunner.run( + device -> device.installApks(extractedApks, installOptions), getDeviceId().get()); } else { - installer.installApks(extractedApks, installOptions); + adbRunner.run(device -> device.installApks(extractedApks, installOptions)); + } + + pushSplits(deviceSpec, extractApksCommand.build(), adbRunner); + } + } + + private void pushSplits( + DeviceSpec baseSpec, ExtractApksCommand baseExtractCommand, AdbRunner adbRunner) { + if (!getPushSplitsPath().isPresent()) { + return; + } + + ExtractApksCommand.Builder extractApksCommand = ExtractApksCommand.builder(); + extractApksCommand.setApksArchivePath(baseExtractCommand.getApksArchivePath()); + baseExtractCommand.getOutputDirectory().ifPresent(extractApksCommand::setOutputDirectory); + + // We want to push all modules... + extractApksCommand.setModules(ImmutableSet.of(ExtractApksCommand.ALL_MODULES_SHORTCUT)); + // ... and all languages + BuildApksResult toc = ResultUtils.readTableOfContents(getApksArchivePath()); + ImmutableSet targetedLanguages = ResultUtils.getAllTargetedLanguages(toc); + final ImmutableList extractedApksForPush = + extractApksCommand + .setDeviceSpec(baseSpec.toBuilder().addAllSupportedLocales(targetedLanguages).build()) + .build() + .execute(); + + Device.PushOptions.Builder pushOptions = + Device.PushOptions.builder() + .setDestinationPath(getPushSplitsPath().get()) + .setClearDestinationPath(getClearPushPath()); + + // We're going to need the package name later for pushing to relative paths + // (i.e. inside the app's private directory) + if (!getPushSplitsPath().get().startsWith("/")) { + String packageName = toc.getPackageName(); + if (packageName.isEmpty()) { + throw new CommandExecutionException( + "Unable to determine the package name of the base APK. If your APK " + + "set was produced using an older version of bundletool, please " + + "regenerate it. Alternatively, you can try again with an " + + "absolute path for --push-splits-to, pointing to a location " + + "that is writeable by the shell user, e.g. /sdcard/..."); } + pushOptions.setPackageName(packageName); + } + + if (getDeviceId().isPresent()) { + adbRunner.run( + device -> device.pushApks(extractedApksForPush, pushOptions.build()), + getDeviceId().get()); + } else { + adbRunner.run(device -> device.pushApks(extractedApksForPush, pushOptions.build())); } } @@ -174,6 +256,20 @@ private void validateInput() { checkFileExistsAndReadable(getApksArchivePath()); } checkFileExistsAndExecutable(getAdbPath()); + if (getClearPushPath()) { + checkArgument( + getPushSplitsPath().isPresent(), + "--%s only applies when --%s is set.", + CLEAR_PUSH_PATH_FLAG.getName(), + PUSH_SPLITS_FLAG.getName()); + } + getPushSplitsPath() + .ifPresent( + path -> + checkArgument( + !path.isEmpty(), + "The value of the flag --%s cannot be empty.", + PUSH_SPLITS_FLAG.getName())); } public static CommandHelp help() { @@ -239,6 +335,14 @@ public static CommandHelp help() { + "value of this flag is ignored if the device receives a standalone APK.", ExtractApksCommand.ALL_MODULES_SHORTCUT) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(ALLOW_TEST_ONLY_FLAG.getName()) + .setOptional(true) + .setDescription( + "If set, apps with 'android:testOnly=true' set in their manifest can also be" + + " deployed") + .build()) .build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/device/ApksInstaller.java b/src/main/java/com/android/tools/build/bundletool/device/AdbRunner.java similarity index 70% rename from src/main/java/com/android/tools/build/bundletool/device/ApksInstaller.java rename to src/main/java/com/android/tools/build/bundletool/device/AdbRunner.java index 0fe36f8e..21e5f4b9 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/ApksInstaller.java +++ b/src/main/java/com/android/tools/build/bundletool/device/AdbRunner.java @@ -18,30 +18,29 @@ import static com.google.common.collect.ImmutableList.toImmutableList; -import com.android.tools.build.bundletool.device.Device.InstallOptions; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.DeviceNotFoundException; import com.android.tools.build.bundletool.model.exceptions.DeviceNotFoundException.TooManyDevicesMatchedException; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; -import java.nio.file.Path; import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import java.util.function.Predicate; -/** Responsible for installing the APKs. */ -public class ApksInstaller { +/** Responsible for running actions on a connected device. */ +public class AdbRunner { private final AdbServer adbServer; /** Initializes the instance. Expects the {@link AdbServer} to be initialized. */ - public ApksInstaller(AdbServer adbServer) { + public AdbRunner(AdbServer adbServer) { this.adbServer = adbServer; } - /** Attempts to install the given APKs to the only connected device. */ - public void installApks(ImmutableList apkPaths, InstallOptions installOptions) { + /** Attempts to run the given action on the only connected device. */ + public void run(Consumer deviceAction) { try { - installApks(apkPaths, installOptions, Predicates.alwaysTrue()); + run(deviceAction, Predicates.alwaysTrue()); } catch (TooManyDevicesMatchedException e) { throw CommandExecutionException.builder() .withMessage("Expected to find one connected device, but found %d.", e.getMatchedNumber()) @@ -55,11 +54,10 @@ public void installApks(ImmutableList apkPaths, InstallOptions installOpti } } - /** Attempts to install the given APKs to a device with a given serial number. */ - public void installApks( - ImmutableList apkPaths, InstallOptions installOptions, String deviceId) { + /** Attempts to run the given action on a device with a given serial number. */ + public void run(Consumer deviceAction, String deviceId) { try { - installApks(apkPaths, installOptions, device -> device.getSerialNumber().equals(deviceId)); + run(deviceAction, device -> device.getSerialNumber().equals(deviceId)); } catch (DeviceNotFoundException e) { throw CommandExecutionException.builder() .withMessage("Expected to find one connected device with serial number '%s'.", deviceId) @@ -68,8 +66,7 @@ public void installApks( } } - private void installApks( - ImmutableList apkPaths, InstallOptions installOptions, Predicate deviceFilter) { + private void run(Consumer deviceAction, Predicate deviceFilter) { try { ImmutableList matchedDevices = adbServer.getDevices().stream().filter(deviceFilter).collect(toImmutableList()); @@ -79,7 +76,7 @@ private void installApks( throw new TooManyDevicesMatchedException(matchedDevices.size()); } - installOnDevice(apkPaths, installOptions, matchedDevices.get(0)); + deviceAction.accept(matchedDevices.get(0)); } catch (TimeoutException e) { throw CommandExecutionException.builder() @@ -88,9 +85,4 @@ private void installApks( .build(); } } - - private void installOnDevice( - ImmutableList apkPaths, InstallOptions installOptions, Device device) { - device.installApks(apkPaths, installOptions); - } } 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 05a991e5..67bcf4bb 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java @@ -18,6 +18,7 @@ import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.addModuleDependencies; import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.buildAdjacencyMap; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NEW_DELIVERY_TYPE_MANIFEST_TAG; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -212,7 +213,7 @@ private ImmutableSet buildModulesDeliveredInstallTime( private boolean willBeDeliveredInstallTime(ModuleMetadata moduleMetadata, Version bundleVersion) { boolean installTime = - bundleVersion.isNewerThan(Version.of("0.10.1")) + NEW_DELIVERY_TYPE_MANIFEST_TAG.enabledForVersion(bundleVersion) ? moduleMetadata.getDeliveryType().equals(DeliveryType.INSTALL_TIME) : !moduleMetadata.getOnDemandDeprecated(); 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 0daa0aa4..8aabcec1 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.device; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import com.android.ddmlib.AdbCommandRejectedException; @@ -23,15 +24,22 @@ import com.android.ddmlib.IDevice.DeviceState; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.InstallException; +import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.SyncException; import com.android.ddmlib.TimeoutException; import com.android.sdklib.AndroidVersion; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.InstallationException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.FormatMethod; +import com.google.errorprone.annotations.FormatString; import java.io.File; import java.io.IOException; +import java.io.PrintStream; import java.nio.file.Path; +import java.util.Arrays; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -108,8 +116,13 @@ public void executeShellCommand( @Override public void installApks(ImmutableList apks, InstallOptions installOptions) { ImmutableList apkFiles = apks.stream().map(Path::toFile).collect(toImmutableList()); - ImmutableList extraArgs = - installOptions.getAllowDowngrade() ? ImmutableList.of("-d") : ImmutableList.of(); + ImmutableList.Builder extraArgs = ImmutableList.builder(); + if (installOptions.getAllowDowngrade()) { + extraArgs.add("-d"); + } + if (installOptions.getAllowTestOnly()) { + extraArgs.add("-t"); + } try { if (getVersion() @@ -117,14 +130,14 @@ public void installApks(ImmutableList apks, InstallOptions installOptions) device.installPackages( apkFiles, installOptions.getAllowReinstall(), - extraArgs, + extraArgs.build(), installOptions.getTimeout().toMillis(), TimeUnit.MILLISECONDS); } else { device.installPackage( Iterables.getOnlyElement(apkFiles).toString(), installOptions.getAllowReinstall(), - extraArgs.toArray(new String[0])); + extraArgs.build().toArray(new String[0])); } } catch (InstallException e) { throw InstallationException.builder() @@ -133,4 +146,183 @@ public void installApks(ImmutableList apks, InstallOptions installOptions) .build(); } } + + @Override + public void pushApks(ImmutableList apks, PushOptions pushOptions) { + String splitsPath = pushOptions.getDestinationPath(); + checkArgument(!splitsPath.isEmpty(), "Splits path cannot be empty."); + + RemoteCommandExecutor commandExecutor = + new RemoteCommandExecutor(this, pushOptions.getTimeout().toMillis(), System.err); + + try { + // There are two different flows, depending on if the path is absolute or not... + if (!pushOptions.getDestinationPath().startsWith("/")) { + // Path is relative, so we're going to try to push it to the app's private dir + // For that, we will need the package name to use with "run-as" command + String packageName = + pushOptions + .getPackageName() + .orElseThrow( + () -> + new CommandExecutionException( + "PushOptions.packageName must be set for relative paths.")); + + // Some clean up first. Remove the destination dir if flag is set... + if (pushOptions.getClearDestinationPath()) { + commandExecutor.executeAsUserAndPrint(packageName, "rm -rf %s", splitsPath); + } + + // ...and recreate it, making sure the destination dir is empty. + // We don't want splits from previous runs in the directory. + // There isn't a nice way to test if dir is empty in shell, but rmdir will return error + commandExecutor.executeAsUserAndPrint( + packageName, + "mkdir -p %s && rmdir %s && mkdir -p %s", + splitsPath, + splitsPath, + splitsPath); + + // The push command further down doesn't support "run-as", so we're going to push + // to a temporary folder, then copy the files to the final destination + String remoteTmpPath = joinUnixPaths("/data/local/tmp/", packageName); + commandExecutor.executeAndPrint("mkdir -p %s", remoteTmpPath); + for (Path apkPath : apks) { + String remoteTmpFilePath = joinUnixPaths(remoteTmpPath, apkPath.getFileName().toString()); + device.pushFile(apkPath.toFile().getAbsolutePath(), remoteTmpFilePath); + + // Now we can copy ("cat" in case there is no "cp" on device) from tmp to splitsPath + // "sh -c" needed to wrap the whole "cat" call because of ">" redirect + commandExecutor.executeAsUserAndPrint( + packageName, + "cat %s > %s", + remoteTmpFilePath, + joinUnixPaths(splitsPath, apkPath.getFileName().toString())); + commandExecutor.executeAndPrint("rm %s", remoteTmpFilePath); + System.err.printf( + "Pushed \"%s\"%n", joinUnixPaths(splitsPath, apkPath.getFileName().toString())); + } + } else { + // Path is absolute. We assume it's pointing to a location writeable by ADB shell, + // which is explained in the bundletool help. It shouldn't point to app's private directory. + + // Some clean up first. Remove the destination dir if flag is set... + if (pushOptions.getClearDestinationPath()) { + commandExecutor.executeAndPrint("rm -rf %s", splitsPath); + } + + // ... and recreate it, making sure the destination dir is empty. + // We don't want splits from previous runs in the directory. + // There isn't a nice way to test if dir is empty in shell, but rmdir will return error + commandExecutor.executeAndPrint( + "mkdir -p %s && rmdir %s && mkdir -p %s", splitsPath, splitsPath, splitsPath); + + // Try to push files normally. Will fail if ADB shell doesn't have permission to write. + for (Path path : apks) { + device.pushFile( + path.toFile().getAbsolutePath(), + joinUnixPaths(splitsPath, path.getFileName().toString())); + System.err.printf( + "Pushed \"%s\"%n", joinUnixPaths(splitsPath, path.getFileName().toString())); + } + } + } catch (IOException + | TimeoutException + | SyncException + | AdbCommandRejectedException + | ShellCommandUnresponsiveException e) { + throw CommandExecutionException.builder() + .withCause(e) + .withMessage( + "Pushing additional splits for offline testing of dynamic features failed. Your app" + + " might still have been installed correctly, but you won't be able to test" + + " with FakeSplitInstallManager.") + .build(); + } + } + + static class RemoteCommandExecutor { + private final Device device; + private final MultiLineReceiver receiver; + private final long timeout; + private final PrintStream out; + private String lastOutputLine; + + RemoteCommandExecutor(Device device, long timeout, PrintStream out) { + this.device = device; + this.timeout = timeout; + this.out = out; + this.receiver = + new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (!line.isEmpty()) { + out.println("ADB >> " + line); + lastOutputLine = line; + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + }; + } + + @FormatMethod + private void executeAndPrint(String commandFormat, String... args) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + String command = formatCommandWithArgs(commandFormat, args); + lastOutputLine = null; + out.println("ADB << " + command); + // ExecuteShellCommand would only tell us about ADB errors, and NOT the actual shell commands + // We need another way to check exit values of the commands we run. + // By adding " && echo OK" we can make sure "OK" is printed if the cmd executed successfully. + device.executeShellCommand(command + " && echo OK", receiver, timeout, TimeUnit.MILLISECONDS); + if (!"OK".equals(lastOutputLine)) { + throw new IOException("ADB command failed."); + } + } + + /** Runs the command as the user of the debuggable app with specified package name. */ + @FormatMethod + private void executeAsUserAndPrint( + String packageName, @FormatString String command, String... args) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + // "run-as packageName" lets us run with the permissions of the app to access its directory + // "sh -c 'cmd'" is needed to be able to use > redirects and && operator inside the cmd + executeAndPrint("run-as %s sh -c %s", packageName, formatCommandWithArgs(command, args)); + } + + /** Returns the string in single quotes, with any single quotes in the string escaped. */ + static String escapeAndSingleQuote(String string) { + return "'" + string.replace("'", "'\\''") + "'"; + } + + /** + * Formats the command string, replacing %s argument placeholders with escaped and single quoted + * args. This was made to work with Strings only, so format your args beforehand. + */ + @FormatMethod + static String formatCommandWithArgs(String command, String... args) { + return String.format( + command, Arrays.stream(args).map(RemoteCommandExecutor::escapeAndSingleQuote).toArray()); + } + } + + // We can't rely on Path and friends if running on Windows, emulator always needs UNIX paths + static String joinUnixPaths(String... parts) { + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '/') { + sb.append('/'); + } + sb.append(part); + } + return sb.toString(); + } } 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 08645282..171bd6d0 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/Device.java +++ b/src/main/java/com/android/tools/build/bundletool/device/Device.java @@ -63,6 +63,8 @@ public abstract void executeShellCommand( public abstract void installApks(ImmutableList apks, InstallOptions installOptions); + public abstract void pushApks(ImmutableList apks, PushOptions installOptions); + /** Options related to APK installation. */ @Immutable @AutoValue @@ -73,12 +75,16 @@ public abstract static class InstallOptions { public abstract boolean getAllowReinstall(); + public abstract boolean getAllowTestOnly(); + public abstract Duration getTimeout(); public static Builder builder() { return new AutoValue_Device_InstallOptions.Builder() .setTimeout(DEFAULT_ADB_TIMEOUT) - .setAllowReinstall(true); + .setAllowReinstall(true) + .setAllowDowngrade(false) + .setAllowTestOnly(false); } /** Builder for {@link InstallOptions}. */ @@ -90,7 +96,43 @@ public abstract static class Builder { public abstract Builder setTimeout(Duration timeout); + public abstract Builder setAllowTestOnly(boolean allowTestOnly); + public abstract InstallOptions build(); } } + + /** Options related to APK installation. */ + @Immutable + @AutoValue + @AutoValue.CopyAnnotations + public abstract static class PushOptions { + public abstract String getDestinationPath(); + + public abstract Duration getTimeout(); + + public abstract Optional getPackageName(); + + public abstract boolean getClearDestinationPath(); + + public static Builder builder() { + return new AutoValue_Device_PushOptions.Builder() + .setTimeout(DEFAULT_ADB_TIMEOUT) + .setClearDestinationPath(false); + } + + /** Builder for {@link InstallOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setDestinationPath(String destinationPath); + + public abstract Builder setTimeout(Duration timeout); + + public abstract Builder setPackageName(String packageName); + + public abstract Builder setClearDestinationPath(boolean shouldClear); + + public abstract PushOptions build(); + } + } } 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 0426bd9b..8ad06a2b 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java @@ -23,6 +23,7 @@ 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; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NO_DEFAULT_UNCOMPRESS_EXTENSIONS; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -47,6 +48,7 @@ import com.android.tools.build.bundletool.model.WearApkLocator; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.utils.PathMatcher; import com.android.tools.build.bundletool.model.utils.Versions; import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.android.tools.build.bundletool.model.version.Version; @@ -61,12 +63,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.nio.file.Paths; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; @@ -130,14 +128,9 @@ final class ApkSerializerHelper { this.bundleVersion = bundleVersion; this.signingConfig = signingConfig; - // Using the default filesystem will work on Windows because the "/" of the glob are swapped - // with "\" when the PathMatcher is constructed and we then use a FileSystem's Path when - // comparing which will thus also use the "\" separator. - FileSystem fileSystem = FileSystems.getDefault(); this.uncompressedPathMatchers = compression.getUncompressedGlobList().stream() - .map(glob -> "glob:" + glob) - .map(fileSystem::getPathMatcher) + .map(PathMatcher::createFromGlob) .collect(toImmutableList()); } @@ -309,10 +302,8 @@ private boolean shouldCompress( boolean uncompressNativeLibs, boolean splitIsAssetSlice, boolean entryShouldCompress) { - // Developer knows best: when they provide the uncompressed glob, we respect it. - // We convert the ZipPath to a FileSystem's path for the PathMatcher to work. if (uncompressedPathMatchers.stream() - .anyMatch(pathMatcher -> pathMatcher.matches(Paths.get(path.toString())))) { + .anyMatch(pathMatcher -> pathMatcher.matches(path.toString()))) { return false; } @@ -328,9 +319,7 @@ private boolean shouldCompress( // Common extensions that should remain uncompressed because compression doesn't provide any // gains. - // For bundle versions starting by 0.7.3 the no-compression is fully configured through the - // bundle config file. - if (bundleVersion.isOlderThan(Version.of("0.7.3")) + if (!NO_DEFAULT_UNCOMPRESS_EXTENSIONS.enabledForVersion(bundleVersion) && NO_COMPRESSION_EXTENSIONS.contains(FileUtils.getFileExtension(path))) { return false; } 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 10cdb28d..baa1ad17 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -105,6 +105,7 @@ public void populateApkSetBuilder( // Finalize the output archive. apkSetBuilder.setTableOfContentsFile( BuildApksResult.newBuilder() + .setPackageName(appBundle.getBaseModule().getAndroidManifest().getPackageName()) .addAllVariant(allVariantsWithTargeting) .setBundletool( Bundletool.newBuilder() 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 246e8b1c..1effe1e2 100755 --- a/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java @@ -22,9 +22,9 @@ import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModuleName.BASE_MODULE_NAME; import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Predicates.not; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.MoreCollectors.onlyElement; import static java.util.stream.Collectors.groupingBy; import com.android.aapt.Resources.ResourceTable; @@ -56,6 +56,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.SetMultimap; +import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import java.io.IOException; import java.io.InputStream; @@ -89,7 +90,7 @@ public ModuleSplitsToShardMerger(DexMerger dexMerger, Path globalTempDir) { /** * Gets a list of collections of splits, and merges each collection into a single standalone APK - * (aka shard) and additional unmatched language splits. + * (aka shard). * * @param unfusedShards a list of lists - each inner list is a collection of splits * @param bundleMetadata the App Bundle metadata @@ -110,7 +111,9 @@ public ImmutableList merge( } /** - * Gets a collections of splits and merges them into a single system APK (aka shard). + * Gets a collections of splits and merges them into a single system APK (aka shard), and + * additionally returns unmatched language splits for fused modules and module splits for unfused + * modules. * * @param splits Collection of generated splits from all modules and configurations. * @param bundleMetadata the App Bundle metadata @@ -124,10 +127,16 @@ public ShardedSystemSplits mergeSystemShard( BundleMetadata bundleMetadata, DeviceSpec deviceSpec) { ApkMatcher deviceSpecMatcher = new ApkMatcher(deviceSpec); - - ImmutableSet splitsForTheSystemApk = + AndroidManifest baseMasterAndroidManifest = getBaseMasterAndroidManifest(splits); + ImmutableSet splitsOfFusedModules = splits.stream() .filter(split -> modulesToFuse.contains(split.getModuleName())) + .collect(toImmutableSet()); + ImmutableSet splitsOfNonFusedModules = + Sets.difference(ImmutableSet.copyOf(splits), splitsOfFusedModules).immutableCopy(); + + ImmutableSet splitsForTheSystemApk = + splitsOfFusedModules.stream() .filter( split -> !split.getApkTargeting().hasLanguageTargeting() @@ -137,10 +146,24 @@ public ShardedSystemSplits mergeSystemShard( ModuleSplit fusedSplit = mergeSingleShard(splitsForTheSystemApk, bundleMetadata, new HashMap<>()); - // List of remaining language and feature splits that weren't fused. + // Groups all the unmatched language splits for fused modules by language and fuse them to + // generate a single split for each language. + ImmutableSet nonMatchedLanguageSplitsForFusedModules = + Sets.difference(splitsOfFusedModules, splitsForTheSystemApk).stream() + .collect(groupingBy(ModuleSplit::getApkTargeting)) + .values() + .stream() + .map( + languageSplits -> + mergeNonMatchedLanguageSplitsForSystemApks( + ImmutableList.copyOf(languageSplits), + bundleMetadata, + baseMasterAndroidManifest)) + .collect(toImmutableSet()); + + // Write split id for splits not fused. ImmutableList otherSplits = - splits.stream() - .filter(not(splitsForTheSystemApk::contains)) + Sets.union(nonMatchedLanguageSplitsForFusedModules, splitsOfNonFusedModules).stream() .collect(groupingBy(ModuleSplit::getModuleName)) .values() .stream() @@ -158,6 +181,20 @@ ModuleSplit mergeSingleShard( ImmutableCollection splitsOfShard, BundleMetadata bundleMetadata, Map, ImmutableList> mergedDexCache) { + return mergeSingleShard( + splitsOfShard, + bundleMetadata, + mergedDexCache, + /* mergedSplitType= */ SplitType.STANDALONE, + /* mergedManifestOverride= */ Optional.empty()); + } + + private ModuleSplit mergeSingleShard( + ImmutableCollection splitsOfShard, + BundleMetadata bundleMetadata, + Map, ImmutableList> mergedDexCache, + SplitType mergedSplitType, + Optional mergedManifestOverride) { ListMultimap dexFilesToMergeByModule = ArrayListMultimap.create(); @@ -199,7 +236,10 @@ ModuleSplit mergeSingleShard( }); } - AndroidManifest mergedAndroidManifest = mergeAndroidManifests(androidManifestsToMergeByModule); + AndroidManifest mergedAndroidManifest = + mergedManifestOverride.isPresent() + ? mergedManifestOverride.get() + : mergeAndroidManifests(androidManifestsToMergeByModule); Collection mergedDexFiles = mergeDexFilesAndCache( @@ -207,17 +247,20 @@ ModuleSplit mergeSingleShard( // Record names of the modules this shard was fused from. ImmutableList fusedModuleNames = getUniqueModuleNames(splitsOfShard); - AndroidManifest finalAndroidManifest = - mergedAndroidManifest.toEditor().setFusedModuleNames(fusedModuleNames).save(); + if (mergedSplitType.equals(SplitType.STANDALONE)) { + mergedAndroidManifest = + mergedAndroidManifest.toEditor().setFusedModuleNames(fusedModuleNames).save(); + } // Construct the final shard. return buildShard( mergedEntriesByPath.values(), mergedDexFiles, mergedSplitTargeting, - finalAndroidManifest, + mergedAndroidManifest, mergedResourceTable, - mergedAssetsConfig); + mergedAssetsConfig, + mergedSplitType); } /** @@ -257,7 +300,8 @@ ModuleSplit mergeSingleApexShard(ImmutableList splitsOfShard) { // An APEX module is made of one module, so any manifest works. splitsOfShard.get(0).getAndroidManifest(), /* mergedResourceTable= */ Optional.empty(), - /* mergedAssetsConfig= */ new HashMap<>()); + /* mergedAssetsConfig= */ new HashMap<>(), + /* 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(); @@ -269,7 +313,8 @@ private ModuleSplit buildShard( ApkTargeting splitTargeting, AndroidManifest androidManifest, Optional mergedResourceTable, - Map mergedAssetsConfig) { + Map mergedAssetsConfig, + SplitType mergedSplitType) { ImmutableList entries = ImmutableList.builder().addAll(entriesByPath).addAll(mergedDexFiles).build(); ModuleSplit.Builder shard = @@ -277,7 +322,7 @@ private ModuleSplit buildShard( .setAndroidManifest(androidManifest) .setEntries(entries) .setApkTargeting(splitTargeting) - .setSplitType(SplitType.STANDALONE) + .setSplitType(mergedSplitType) // We don't care about the following properties for shards. The values are set just to // satisfy contract of @AutoValue.Builder. // `nativeConfig` is optional and therefore not being set. @@ -427,7 +472,6 @@ private Optional writeMainDexListFileIfPresent(BundleMetadata bundleMetada private static AndroidManifest getOnlyBaseAndroidManifest( SetMultimap manifestsToMergeByModule) { - Set baseManifests = manifestsToMergeByModule.get(BASE_MODULE_NAME); if (baseManifests.size() != 1) { @@ -478,4 +522,27 @@ private static Stream writeSplitIdInManifestHavingSameModule( return splits.stream() .map(split -> split.writeSplitIdInManifest(suffixManager.createSuffix(split))); } + + private ModuleSplit mergeNonMatchedLanguageSplitsForSystemApks( + ImmutableCollection splits, + BundleMetadata bundleMetadata, + AndroidManifest baseMasterAndroidManifest) { + // For the config splits the manifest is overridden later so we use the base master manifest + // initially for the fused split as base manifest might be missing in the splits we are trying + // to merge. + return mergeSingleShard( + splits, + bundleMetadata, + new HashMap<>(), + /* mergedSplitType= */ SplitType.SPLIT, + /* mergedManifestOverride= */ Optional.of(baseMasterAndroidManifest)); + } + + private static AndroidManifest getBaseMasterAndroidManifest( + ImmutableCollection splits) { + return splits.stream() + .filter(split -> split.isMasterSplit() && split.isBaseModuleSplit()) + .collect(onlyElement()) + .getAndroidManifest(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java index 281db7c3..7bcec9ca 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NAMESPACE_ON_INCLUDE_ATTRIBUTE_REQUIRED; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -24,13 +25,13 @@ import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestFusingException.FusingMissingIncludeAttribute; -import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionException.VersionCodeMissingException; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.model.version.VersionGuardedFeature; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.annotations.VisibleForTesting; @@ -178,7 +179,7 @@ public static AndroidManifest create(XmlNode manifestRoot) { */ public static AndroidManifest createForConfigSplit( String packageName, - int versionCode, + Optional versionCode, String splitId, String featureSplitId, Optional extractNativeLibs) { @@ -190,11 +191,11 @@ public static AndroidManifest createForConfigSplit( ManifestEditor editor = new ManifestEditor(createMinimalManifestTag(), BundleToolVersion.getCurrentVersion()) .setPackage(packageName) - .setVersionCode(versionCode) .setSplitId(splitId) .setConfigForSplit(featureSplitId) .setHasCode(false); + versionCode.ifPresent(editor::setVersionCode); extractNativeLibs.ifPresent(editor::setExtractNativeLibsValue); return editor.save(); @@ -330,13 +331,14 @@ public Optional getIsModuleIncludedInFusing() { .flatMap(module -> module.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "fusing")) .map( fusing -> { - if (getBundleToolVersion().isOlderThan(Version.of("0.3.4-dev"))) { + if (NAMESPACE_ON_INCLUDE_ATTRIBUTE_REQUIRED.enabledForVersion( + getBundleToolVersion())) { return fusing - .getAttributeIgnoringNamespace("include") + .getAttribute(DISTRIBUTION_NAMESPACE_URI, "include") .orElseThrow(() -> new FusingMissingIncludeAttribute(getSplitId())); } else { return fusing - .getAttribute(DISTRIBUTION_NAMESPACE_URI, "include") + .getAttributeIgnoringNamespace("include") .orElseThrow(() -> new FusingMissingIncludeAttribute(getSplitId())); } }) @@ -356,11 +358,15 @@ public String getPackageName() { .getValueAsString(); } - public int getVersionCode() { + /** + * Returns the version code. + * + *

Note: Version code is not present for non-upfront asset slices. + */ + public Optional getVersionCode() { return getManifestElement() .getAndroidAttribute(VERSION_CODE_RESOURCE_ID) - .orElseThrow(() -> new VersionCodeMissingException()) - .getValueAsDecimalInteger(); + .map(XmlProtoAttribute::getValueAsDecimalInteger); } public Optional getSplitId() { @@ -394,10 +400,11 @@ public Optional getOnDemandAttribute() { .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "module") .flatMap( module -> { - if (getBundleToolVersion().isOlderThan(Version.of("0.3.4-dev"))) { - return module.getAttributeIgnoringNamespace("onDemand"); - } else { + if (VersionGuardedFeature.NAMESPACE_ON_INCLUDE_ATTRIBUTE_REQUIRED.enabledForVersion( + getBundleToolVersion())) { return module.getAttribute(DISTRIBUTION_NAMESPACE_URI, "onDemand"); + } else { + return module.getAttributeIgnoringNamespace("onDemand"); } }); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java index 9eb13798..3a13bd7f 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.ABI_SANITIZER_DISABLED; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -271,7 +272,7 @@ private static BundleMetadata readBundleMetadata(ZipFile bundleFile) { private static ImmutableList sanitize( ImmutableList modules, BundleConfig bundleConfig) { Version bundleVersion = BundleToolVersion.getVersionFromBundleConfig(bundleConfig); - if (bundleVersion.isOlderThan(Version.of("0.3.1"))) { + if (!ABI_SANITIZER_DISABLED.enabledForVersion(bundleVersion)) { // This is a temporary fix to cope with inconsistent ABIs. modules = modules.stream().map(new ModuleAbiSanitizer()::sanitize).collect(toImmutableList()); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java index fcf4c74d..641c902c 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java +++ b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java @@ -75,7 +75,11 @@ public abstract class BundleModule { public static final ZipPath APEX_DIRECTORY = ZipPath.create("apex"); /** The file of an App Bundle module that contains the APEX manifest. */ - public static final ZipPath APEX_MANIFEST_PATH = ZipPath.create("root/apex_manifest.json"); + public static final ZipPath APEX_MANIFEST_PATH = ZipPath.create("root/apex_manifest.pb"); + public static final ZipPath APEX_MANIFEST_JSON_PATH = ZipPath.create("root/apex_manifest.json"); + + /** The public key used to sign the apex */ + public static final ZipPath APEX_PUBKEY_PATH = ZipPath.create("root/apex_pubkey"); /** The NOTICE file of an APEX Bundle module. */ public static final ZipPath APEX_NOTICE_PATH = ZipPath.create("assets/NOTICE.html.gz"); 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 305f5e7c..5c26ff93 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java @@ -23,6 +23,8 @@ import static com.android.tools.build.bundletool.model.BundleModule.RESOURCES_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.ROOT_DIRECTORY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.SCREEN_DENSITY_TO_PROTO_VALUE_MAP; +import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeApkTargeting; +import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeVariantTargeting; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.lPlusVariantTargeting; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; @@ -83,10 +85,18 @@ public enum SplitType { ASSET_SLICE, } - /** Returns the targeting of the APK represented by this instance. */ + /** + * Returns the targeting of the APK represented by this instance. + * + *

Order of repeated all repeated fields is guaranteed to be deterministic. + */ public abstract ApkTargeting getApkTargeting(); - /** Returns the targeting of the Variant this instance belongs to. */ + /** + * Returns the targeting of the Variant this instance belongs to. + * + *

Order of repeated all repeated fields is guaranteed to be deterministic. + */ public abstract VariantTargeting getVariantTargeting(); /** Whether this ModuleSplit instance represents a standalone, split, instant or system apk. */ @@ -487,6 +497,16 @@ public Stream getEntriesInDirectory(ZipPath directory) { */ public Stream findEntriesUnderPath(String path) { ZipPath zipPath = ZipPath.create(path); + return findEntriesUnderPath(zipPath); + } + + /** + * Returns all {@link ModuleEntry} that have a relative module path under a given path. + * + *

Note: Consider using {@link #getEntriesByDirectory()} for performance, unless a recursive + * search is truly needed. + */ + public Stream findEntriesUnderPath(ZipPath zipPath) { return getEntriesByDirectory().asMap().entrySet().stream() .filter(dirAndEntries -> dirAndEntries.getKey().startsWith(zipPath)) .flatMap(dirAndEntries -> dirAndEntries.getValue().stream()); @@ -525,8 +545,12 @@ public abstract static class Builder { */ public abstract Builder setApexConfig(ApexImages apexConfig); + protected abstract ApkTargeting getApkTargeting(); + public abstract Builder setApkTargeting(ApkTargeting targeting); + protected abstract VariantTargeting getVariantTargeting(); + public abstract Builder setVariantTargeting(VariantTargeting targeting); public abstract Builder setSplitType(SplitType splitType); @@ -547,7 +571,10 @@ public Builder addMasterManifestMutator(ManifestMutator manifestMutator) { protected abstract ModuleSplit autoBuild(); public ModuleSplit build() { - ModuleSplit moduleSplit = autoBuild(); + ModuleSplit moduleSplit = + this.setApkTargeting(normalizeApkTargeting(getApkTargeting())) + .setVariantTargeting(normalizeVariantTargeting(getVariantTargeting())) + .autoBuild(); // For system splits the master split is formed by fusing Screen Density, Abi, Language // splits, hence it might have Abi, Screen Density, Language targeting set. if (moduleSplit.isMasterSplit() && !moduleSplit.getSplitType().equals(SplitType.SYSTEM)) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java b/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java index 1be4898a..b45ed771 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ZipPath.java @@ -27,17 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.file.FileSystem; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.util.Comparator; -import java.util.Iterator; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -49,7 +39,7 @@ @Immutable @AutoValue @AutoValue.CopyAnnotations -public abstract class ZipPath implements Path { +public abstract class ZipPath implements Comparable { private static final String SEPARATOR = "/"; private static final Splitter SPLITTER = Splitter.on(SEPARATOR).omitEmptyStrings(); @@ -66,7 +56,7 @@ public abstract class ZipPath implements Path { * *

Note that this list can be empty when denoting the root of the zip. */ - abstract ImmutableList getNames(); + public abstract ImmutableList getNames(); public static ZipPath create(String path) { checkNotNull(path, "Path cannot be null."); @@ -86,36 +76,29 @@ public static ZipPath create(ImmutableList names) { return new AutoValue_ZipPath(names); } - @Override @CheckReturnValue - public ZipPath resolve(Path p) { + public ZipPath resolve(ZipPath p) { checkNotNull(p, "Path cannot be null."); - ZipPath path = (ZipPath) p; - return create( - ImmutableList.builder().addAll(getNames()).addAll(path.getNames()).build()); + return create(ImmutableList.builder().addAll(getNames()).addAll(p.getNames()).build()); } - @Override @CheckReturnValue public ZipPath resolve(String path) { return resolve(ZipPath.create(path)); } - @Override @CheckReturnValue - public ZipPath resolveSibling(Path path) { + public ZipPath resolveSibling(ZipPath path) { checkNotNull(path, "Path cannot be null."); checkState(!getNames().isEmpty(), "Root has not sibling."); return getParent().resolve(path); } - @Override @CheckReturnValue public ZipPath resolveSibling(String path) { return resolveSibling(ZipPath.create(path)); } - @Override @CheckReturnValue public ZipPath subpath(int from, int to) { checkArgument(from >= 0 && from < getNames().size()); @@ -124,7 +107,6 @@ public ZipPath subpath(int from, int to) { return create(getNames().subList(from, to)); } - @Override @Nullable @CheckReturnValue @Memoized @@ -135,32 +117,27 @@ public ZipPath getParent() { return create(getNames().subList(0, getNames().size() - 1)); } - @Override public int getNameCount() { return getNames().size(); } - @Override public ZipPath getRoot() { return ROOT; } - @Override public ZipPath getName(int index) { checkArgument(index >= 0 && index < getNames().size()); return ZipPath.create(getNames().get(index)); } - @Override - public boolean startsWith(Path p) { - ZipPath path = (ZipPath) p; - if (path.getNameCount() > getNameCount()) { + public boolean startsWith(ZipPath p) { + if (p.getNameCount() > getNameCount()) { return false; } ImmutableList names = getNames(); - ImmutableList otherNames = path.getNames(); - for (int i = 0; i < path.getNameCount(); i++) { + ImmutableList otherNames = p.getNames(); + for (int i = 0; i < p.getNameCount(); i++) { if (!otherNames.get(i).equals(names.get(i))) { return false; } @@ -169,21 +146,18 @@ public boolean startsWith(Path p) { return true; } - @Override public boolean startsWith(String p) { return startsWith(ZipPath.create(p)); } - @Override - public boolean endsWith(Path p) { - ZipPath path = (ZipPath) p; - if (path.getNameCount() > getNameCount()) { + public boolean endsWith(ZipPath p) { + if (p.getNameCount() > getNameCount()) { return false; } ImmutableList names = getNames(); - ImmutableList otherNames = path.getNames(); - for (int i = 0; i < path.getNameCount(); i++) { + ImmutableList otherNames = p.getNames(); + for (int i = 0; i < p.getNameCount(); i++) { if (!otherNames.get(otherNames.size() - i - 1).equals(names.get(names.size() - i - 1))) { return false; } @@ -192,15 +166,14 @@ public boolean endsWith(Path p) { return true; } - @Override public boolean endsWith(String p) { return endsWith(ZipPath.create(p)); } @Override - public final int compareTo(Path other) { + public final int compareTo(ZipPath other) { return Comparators.lexicographical(Comparator.naturalOrder()) - .compare(getNames(), ((ZipPath) other).getNames()); + .compare(getNames(), other.getNames()); } /** Returns the path as used in the zip file. */ @@ -210,69 +183,8 @@ public final String toString() { } @Memoized - @Override public ZipPath getFileName() { checkArgument(getNameCount() > 0, "Root does not have a file name."); return getName(getNameCount() - 1); } - - @Override - public Iterator iterator() { - return getNames().stream().map(name -> (Path) ZipPath.create(name)).iterator(); - } - - @Override - @CheckReturnValue - public ZipPath normalize() { - // We don't support ".." or ".", so the current path is already normalized. - return this; - } - - @Override - public ZipPath toRealPath(LinkOption... options) { - return this; - } - - @Override - public ZipPath toAbsolutePath() { - return this; - } - - @Override - public boolean isAbsolute() { - return true; - } - - @Override - @CheckReturnValue - public ZipPath relativize(Path p) { - throw new UnsupportedOperationException(); - } - - @Override - public WatchKey register(WatchService watcher, WatchEvent.Kind... events) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public WatchKey register( - WatchService watcher, WatchEvent.Kind[] events, WatchEvent.Modifier... modifiers) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public File toFile() { - throw new UnsupportedOperationException("Zip entries don't match to a file on disk."); - } - - @Override - public URI toUri() { - throw new UnsupportedOperationException(); - } - - @Override - public FileSystem getFileSystem() { - throw new UnsupportedOperationException(); - } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java index b2ddb144..4d282aa2 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/ScreenDensitySelector.java @@ -21,6 +21,7 @@ import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.DENSITY_ALIAS_TO_DPI_MAP; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MDPI_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.NONE_DENSITY_VALUE; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.PREFER_EXPLICIT_DPI_OVER_DEFAULT_CONFIG; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -207,7 +208,7 @@ private Comparator comparatorForConfigValues(int desiredDpi, Versio Comparator compositeComparator = Comparator.comparing( ScreenDensitySelector::getDpiValue, new ScreenDensityComparator(desiredDpi)); - if (!bundleVersion.isOlderThan(Version.of("0.9.1"))) { + if (PREFER_EXPLICIT_DPI_OVER_DEFAULT_CONFIG.enabledForVersion(bundleVersion)) { compositeComparator = compositeComparator.thenComparing(ScreenDensitySelector::isExplicitDpi, falseFirst()); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java index 441ff1b6..14655a3a 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java @@ -24,7 +24,6 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import com.google.common.collect.Streams; import com.google.errorprone.annotations.Immutable; import java.util.HashSet; import java.util.Set; @@ -100,8 +99,8 @@ public static TargetedDirectory parse(ZipPath directoryPath) { checkArgument(directoryPath.getNameCount() > 0, "Empty paths are not supported."); ImmutableList segments = - Streams.stream(directoryPath) - .map(path -> TargetedDirectorySegment.parse((ZipPath) path)) + directoryPath.getNames().stream() + .map(TargetedDirectorySegment::parse) .collect(toImmutableList()); checkNoDuplicateDimensions(segments, directoryPath); diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java index 5ac08a93..cd162972 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java @@ -23,7 +23,6 @@ import com.android.bundle.Targeting.GraphicsApi; import com.android.bundle.Targeting.GraphicsApiTargeting; import com.android.bundle.Targeting.LanguageTargeting; -import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.utils.GraphicsApiUtils; import com.android.tools.build.bundletool.model.utils.TextureCompressionUtils; @@ -108,12 +107,11 @@ && getTargeting().hasTextureCompressionFormat()) { return new AutoValue_TargetedDirectorySegment(getName(), newTargeting.build()); } - public static TargetedDirectorySegment parse(ZipPath directorySegment) { - checkArgument(directorySegment.getNameCount() == 1); - if (!directorySegment.toString().contains("#")) { - return TargetedDirectorySegment.create(directorySegment.toString()); + public static TargetedDirectorySegment parse(String directorySegment) { + if (!directorySegment.contains("#")) { + return TargetedDirectorySegment.create(directorySegment); } - Matcher matcher = DIRECTORY_SEGMENT_PATTERN.matcher(directorySegment.toString()); + Matcher matcher = DIRECTORY_SEGMENT_PATTERN.matcher(directorySegment); if (matcher.matches()) { return TargetedDirectorySegment.create( matcher.group("base"), matcher.group("key"), matcher.group("value")); @@ -143,10 +141,9 @@ public String toPathSegment() { } /** - * Fast check (without parsing) that verifies if a dimension can be targeted in a path. - * If this returns true, you should construct a TargetedDirectory from the path to do any work on - * it. If this returns false, the dimension is guaranteed not to be targeted in the specified - * path. + * Fast check (without parsing) that verifies if a dimension can be targeted in a path. If this + * returns true, you should construct a TargetedDirectory from the path to do any work on it. If + * this returns false, the dimension is guaranteed not to be targeted in the specified path. */ public static boolean pathMayContain(String path, TargetingDimension dimension) { Collection keys = DIMENSION_TO_KEY.get(dimension); diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java index fe24d39f..94124f35 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model.targeting; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionTargeting; +import static com.android.tools.build.bundletool.model.utils.TextureCompressionUtils.TEXTURE_TO_TARGETING; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -189,6 +190,35 @@ private static VariantTargeting sdkVariantTargeting(int minSdk) { .build(); } + /** + * Update the module to have the targeting specified by a default value for a dimension. This + * means that the Apk and Variant targeting for this module will have the value passed for this + * dimension - or none if the value is empty. + */ + public static ModuleSplit setTargetingByDefaultSuffix( + ModuleSplit moduleSplit, TargetingDimension dimension, String value) { + // Only TCF is supported for now in targeting by a default suffix. + checkArgument(dimension.equals(TargetingDimension.TEXTURE_COMPRESSION_FORMAT)); + + // If the value is empty, we don't need to modify the targeting of the module split. + if (value.isEmpty()) { + return moduleSplit; + } + + // Apply the updated targeting to the module split (as it now only contains assets for + // the selected TCF), both for the APK and the variant targeting. + return moduleSplit.toBuilder() + .setApkTargeting( + moduleSplit.getApkTargeting().toBuilder() + .setTextureCompressionFormatTargeting(TEXTURE_TO_TARGETING.get(value)) + .build()) + .setVariantTargeting( + moduleSplit.getVariantTargeting().toBuilder() + .setTextureCompressionFormatTargeting(TEXTURE_TO_TARGETING.get(value)) + .build()) + .build(); + } + /** * Update the module to remove the specified targeting from the assets - both the directories in * assets config and the associated module entries having the specified targeting will be updated. @@ -325,30 +355,36 @@ private static ModuleEntry removeTargetingFromEntry( * the dimension, or targeting another value). */ private static boolean isDirectoryTargetingOtherValue( - TargetedAssetsDirectory directory, TargetingDimension dimension, String value) { + TargetedAssetsDirectory directory, TargetingDimension dimension, String searchedValue) { // Only TCF is supported for now in targeting detection. checkArgument(dimension.equals(TargetingDimension.TEXTURE_COMPRESSION_FORMAT)); AssetsDirectoryTargeting targeting = directory.getTargeting(); if (!targeting.hasTextureCompressionFormat()) { + // The directory is not even targeting the specified dimension, + // so it's not targeting another value for this dimension. return false; } - if (targeting.getTextureCompressionFormat().getValueList().isEmpty()) { - // If no value is specified for this directory, it means that it is a fallback for other - // sibling directories containing alternative TCFs. By doing so, it is targeting another value - // than the one passed as parameter. - return !targeting.getTextureCompressionFormat().getAlternativesList().isEmpty(); + // If no value is specified for this directory, it means that it is a fallback for other + // sibling directories containing alternative TCFs. + // Similarly, an empty searched value means that we're looking for fallback directories. + boolean isDirectoryValueFallback = + targeting.getTextureCompressionFormat().getValueList().isEmpty(); + boolean isSearchedValueFallback = searchedValue.isEmpty(); + if (isSearchedValueFallback || isDirectoryValueFallback) { + return isSearchedValueFallback != isDirectoryValueFallback; } - // A value was specified, read it and check if it's the same as the one passed as parameter. + // If a searched value is specified, and the directory has a value for this dimension too, + // read it and check if it's the same as the searched one. String targetingValue = TextureCompressionUtils.TARGETING_TO_TEXTURE.getOrDefault( Iterables.getOnlyElement(targeting.getTextureCompressionFormat().getValueList()) .getAlias(), null); - return !value.equals(targetingValue); + return !searchedValue.equals(targetingValue); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/PathMatcher.java b/src/main/java/com/android/tools/build/bundletool/model/utils/PathMatcher.java new file mode 100755 index 00000000..252ec132 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/PathMatcher.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model.utils; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableSet; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Utility class to match a file path against a glob pattern. + * + *

In a glob pattern: + *

  • {@code *} matches anything without crossing the directory boundary. + *
  • {@code **} matches anything crossing the directory boundary. + *
  • {@code ?} matches exactly one character. + *
  • {@code [...]} matches the set of characters inside the square brackets. Ranges such as "a-z", + * "A-Z" and "0-9" are also supported. The character "/" (forward slash) is not allowed within + * square brackets. + *
  • { ... , ... } matches any of the sub-patterns separated by commas in between the + * curly braces. + */ +public final class PathMatcher { + + /** Special characters interpreted by regexp engines. */ + private static final ImmutableSet REGEXP_SPECIAL_CHARS = + "<([{\\^-=$!|]})?*+.>".chars().mapToObj(c -> (char) c).collect(toImmutableSet()); + + private final Pattern regexpPattern; + + private PathMatcher(Pattern regexpPattern) { + this.regexpPattern = regexpPattern; + } + + /** Builds an instance of {@link PathMatcher} that will match the given globPattern pattern. */ + public static PathMatcher createFromGlob(String globPattern) { + try { + Pattern regexpPattern = Pattern.compile(convertGlobToRegexp(globPattern)); + return new PathMatcher(regexpPattern); + } catch (PatternSyntaxException e) { + throw new GlobPatternSyntaxException(globPattern, e); + } + } + + public boolean matches(String input) { + return regexpPattern.matcher(input).matches(); + } + + private static String convertGlobToRegexp(String globPattern) { + StringBuilder regexpBuilder = new StringBuilder().append('^'); + + boolean inGroup = false; + int openingGroupIdx = 0; + int i = 0; + while (i < globPattern.length()) { + switch (globPattern.charAt(i)) { + case '\\': + if (i == globPattern.length() - 1) { + throw new GlobPatternSyntaxException("No character to escape.", globPattern, i); + } + regexpBuilder.append('\\').append(globPattern.charAt(i + 1)); + i++; + break; + + case '*': + if (i + 1 < globPattern.length() && globPattern.charAt(i + 1) == '*') { + i++; + regexpBuilder.append(".*?"); + } else { + regexpBuilder.append("[^/]*"); + } + break; + + case '?': + regexpBuilder.append("."); + break; + + case '[': + int openBracketIdx = i; + regexpBuilder.append('['); + + i++; + char nextChar = i < globPattern.length() ? globPattern.charAt(i) : 0; + if (nextChar == '^') { + regexpBuilder.append('\\'); + } else if (nextChar == '!') { + regexpBuilder.append('^'); + } + + while (i < globPattern.length() && globPattern.charAt(i) != ']') { + char currentChar = globPattern.charAt(i); + if (currentChar == '/') { + throw new GlobPatternSyntaxException( + "Character '/' is not allowed within a character set", globPattern, i); + } + regexpBuilder.append(globPattern.charAt(i)); + i++; + } + if (i == globPattern.length()) { + throw new GlobPatternSyntaxException( + "No matching ']' found.", globPattern, openBracketIdx); + } + if (i == openBracketIdx + 1) { + throw new GlobPatternSyntaxException( + "Empty characters set.", globPattern, openBracketIdx); + } + regexpBuilder.append(globPattern.charAt(i)); + break; + + case '{': + if (inGroup) { + throw new GlobPatternSyntaxException("Cannot nest groups.", globPattern, i); + } + openingGroupIdx = i; + inGroup = true; + regexpBuilder.append("(?:"); + break; + + case '}': + if (!inGroup) { + throw new GlobPatternSyntaxException("No matching '{' found.", globPattern, i); + } + regexpBuilder.append(')'); + inGroup = false; + break; + + case ']': + throw new GlobPatternSyntaxException("No matching '[' found.", globPattern, i); + + case ',': + if (inGroup) { + regexpBuilder.append('|'); + } else { + regexpBuilder.append(','); + } + break; + + default: + char currentChar = globPattern.charAt(i); + if (REGEXP_SPECIAL_CHARS.contains(currentChar)) { + regexpBuilder.append('\\'); + } + regexpBuilder.append(currentChar); + } + + i++; + } + + if (inGroup) { + throw new GlobPatternSyntaxException("No matching '}' found.", globPattern, openingGroupIdx); + } + + return regexpBuilder.append('$').toString(); + } + + /** Exception indicating that a glob pattern could not be parsed. */ + public static class GlobPatternSyntaxException extends RuntimeException { + + private GlobPatternSyntaxException(String message, String globPattern, int index) { + super( + String.format( + "Unable to parse glob pattern '%s' at character %d. Error: %s", + globPattern, index + 1, message)); + } + + private GlobPatternSyntaxException(String globPattern, Throwable cause) { + super(String.format("Unable to parse glob pattern '%s'.", globPattern), cause); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java index 585ddc01..8909ff01 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java @@ -18,12 +18,15 @@ import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_FILE; 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.BuildApksResult; import com.android.bundle.Commands.Variant; import com.android.tools.build.bundletool.model.utils.files.BufferedIo; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -146,5 +149,18 @@ public static boolean isSystemApkVariant(Variant variant) { .anyMatch(ApkDescription::hasSystemApkMetadata); } + public static ImmutableSet getAllTargetedLanguages(BuildApksResult result) { + return Streams.concat( + result.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()), + result.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream())) + .flatMap( + apkDescription -> + apkDescription.getTargeting().getLanguageTargeting().getValueList().stream()) + .collect(toImmutableSet()); + } + private ResultUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java new file mode 100755 index 00000000..2db1a6e0 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model.utils; + +import static com.google.common.collect.Comparators.lexicographical; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Comparator.comparing; + +import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.GraphicsApi; +import com.android.bundle.Targeting.GraphicsApiTargeting; +import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.MultiAbi; +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.bundle.Targeting.Sanitizer; +import com.android.bundle.Targeting.SanitizerTargeting; +import com.android.bundle.Targeting.ScreenDensity; +import com.android.bundle.Targeting.ScreenDensityTargeting; +import com.android.bundle.Targeting.SdkVersion; +import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.bundle.Targeting.TextureCompressionFormat; +import com.android.bundle.Targeting.TextureCompressionFormatTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.google.common.collect.ImmutableList; +import java.util.Comparator; + +/** Helpers related to APK resources qualifiers. */ +public final class TargetingNormalizer { + + private static final Comparator ABI_COMPARATOR = comparing(Abi::getAlias); + + private static final Comparator GRAPHICS_API_COMPARATOR = + Comparator.comparing(GraphicsApi::getApiOneofCase) + .thenComparing(graphicsApi -> graphicsApi.getMinOpenGlVersion().getMajor()) + .thenComparing(graphicsApi -> graphicsApi.getMinOpenGlVersion().getMinor()) + .thenComparing(graphicsApi -> graphicsApi.getMinVulkanVersion().getMajor()) + .thenComparing(graphicsApi -> graphicsApi.getMinVulkanVersion().getMinor()); + + private static final Comparator MULTI_ABI_COMPARATOR = + comparing(MultiAbi::getAbiList, lexicographical(comparing(Abi::getAlias))); + + private static final Comparator SANITIZER_COMPARATOR = comparing(Sanitizer::getAlias); + + private static final Comparator SCREEN_DENSITY_COMPARATOR = + Comparator.comparingInt(ResourcesUtils::convertToDpi); + + private static final Comparator SDK_VERSION_COMPARATOR = + comparing(sdkVersion -> sdkVersion.getMin().getValue()); + + private static final Comparator TEXTURE_COMPRESSION_FORMAT_COMPARATOR = + comparing(TextureCompressionFormat::getAlias); + + public static ApkTargeting normalizeApkTargeting(ApkTargeting targeting) { + ApkTargeting.Builder normalized = targeting.toBuilder(); + if (targeting.hasAbiTargeting()) { + normalized.setAbiTargeting(normalizeAbiTargeting(targeting.getAbiTargeting())); + } + if (targeting.hasLanguageTargeting()) { + normalized.setLanguageTargeting(normalizeLanguageTargeting(targeting.getLanguageTargeting())); + } + if (targeting.hasGraphicsApiTargeting()) { + normalized.setGraphicsApiTargeting( + normalizeGraphicsApiTargeting(targeting.getGraphicsApiTargeting())); + } + if (targeting.hasMultiAbiTargeting()) { + normalized.setMultiAbiTargeting(normalizeMultiAbiTargeting(targeting.getMultiAbiTargeting())); + } + if (targeting.hasSanitizerTargeting()) { + normalized.setSanitizerTargeting( + normalizeSanitizerTargeting(targeting.getSanitizerTargeting())); + } + if (targeting.hasScreenDensityTargeting()) { + normalized.setScreenDensityTargeting( + normalizeScreenDensityTargeting(targeting.getScreenDensityTargeting())); + } + if (targeting.hasSdkVersionTargeting()) { + normalized.setSdkVersionTargeting( + normalizeSdkVersionTargeting(targeting.getSdkVersionTargeting())); + } + if (targeting.hasTextureCompressionFormatTargeting()) { + normalized.setTextureCompressionFormatTargeting( + normalizeTextureCompressionFormatTargeting( + targeting.getTextureCompressionFormatTargeting())); + } + return normalized.build(); + } + + public static VariantTargeting normalizeVariantTargeting(VariantTargeting targeting) { + VariantTargeting.Builder normalized = targeting.toBuilder(); + if (targeting.hasAbiTargeting()) { + normalized.setAbiTargeting(normalizeAbiTargeting(targeting.getAbiTargeting())); + } + if (targeting.hasMultiAbiTargeting()) { + normalized.setMultiAbiTargeting(normalizeMultiAbiTargeting(targeting.getMultiAbiTargeting())); + } + if (targeting.hasScreenDensityTargeting()) { + normalized.setScreenDensityTargeting( + normalizeScreenDensityTargeting(targeting.getScreenDensityTargeting())); + } + if (targeting.hasSdkVersionTargeting()) { + normalized.setSdkVersionTargeting( + normalizeSdkVersionTargeting(targeting.getSdkVersionTargeting())); + } + if (targeting.hasTextureCompressionFormatTargeting()) { + normalized.setTextureCompressionFormatTargeting( + normalizeTextureCompressionFormatTargeting( + targeting.getTextureCompressionFormatTargeting())); + } + return normalized.build(); + } + + private static AbiTargeting normalizeAbiTargeting(AbiTargeting targeting) { + return AbiTargeting.newBuilder() + .addAllValue(ImmutableList.sortedCopyOf(ABI_COMPARATOR, targeting.getValueList())) + .addAllAlternatives( + ImmutableList.sortedCopyOf(ABI_COMPARATOR, targeting.getAlternativesList())) + .build(); + } + + private static GraphicsApiTargeting normalizeGraphicsApiTargeting( + GraphicsApiTargeting targeting) { + return GraphicsApiTargeting.newBuilder() + .addAllValue(ImmutableList.sortedCopyOf(GRAPHICS_API_COMPARATOR, targeting.getValueList())) + .addAllAlternatives( + ImmutableList.sortedCopyOf(GRAPHICS_API_COMPARATOR, targeting.getAlternativesList())) + .build(); + } + + private static LanguageTargeting normalizeLanguageTargeting(LanguageTargeting targeting) { + return LanguageTargeting.newBuilder() + .addAllValue(ImmutableList.sortedCopyOf(targeting.getValueList())) + .addAllAlternatives(ImmutableList.sortedCopyOf(targeting.getAlternativesList())) + .build(); + } + + private static MultiAbiTargeting normalizeMultiAbiTargeting(MultiAbiTargeting targeting) { + return MultiAbiTargeting.newBuilder() + .addAllValue( + targeting.getValueList().stream() + .map(TargetingNormalizer::normalizeMultiAbi) + .sorted(MULTI_ABI_COMPARATOR) + .collect(toImmutableList())) + .addAllAlternatives( + targeting.getAlternativesList().stream() + .map(TargetingNormalizer::normalizeMultiAbi) + .sorted(MULTI_ABI_COMPARATOR) + .collect(toImmutableList())) + .build(); + } + + private static MultiAbi normalizeMultiAbi(MultiAbi targeting) { + return MultiAbi.newBuilder() + .addAllAbi(ImmutableList.sortedCopyOf(ABI_COMPARATOR, targeting.getAbiList())) + .build(); + } + + private static SanitizerTargeting normalizeSanitizerTargeting(SanitizerTargeting targeting) { + return SanitizerTargeting.newBuilder() + .addAllValue(ImmutableList.sortedCopyOf(SANITIZER_COMPARATOR, targeting.getValueList())) + .build(); + } + + private static ScreenDensityTargeting normalizeScreenDensityTargeting( + ScreenDensityTargeting targeting) { + return ScreenDensityTargeting.newBuilder() + .addAllValue( + ImmutableList.sortedCopyOf(SCREEN_DENSITY_COMPARATOR, targeting.getValueList())) + .addAllAlternatives( + ImmutableList.sortedCopyOf(SCREEN_DENSITY_COMPARATOR, targeting.getAlternativesList())) + .build(); + } + + private static SdkVersionTargeting normalizeSdkVersionTargeting(SdkVersionTargeting targeting) { + return SdkVersionTargeting.newBuilder() + .addAllValue(ImmutableList.sortedCopyOf(SDK_VERSION_COMPARATOR, targeting.getValueList())) + .addAllAlternatives( + ImmutableList.sortedCopyOf(SDK_VERSION_COMPARATOR, targeting.getAlternativesList())) + .build(); + } + + private static TextureCompressionFormatTargeting normalizeTextureCompressionFormatTargeting( + TextureCompressionFormatTargeting targeting) { + return TextureCompressionFormatTargeting.newBuilder() + .addAllValue( + ImmutableList.sortedCopyOf( + TEXTURE_COMPRESSION_FORMAT_COMPARATOR, targeting.getValueList())) + .addAllAlternatives( + ImmutableList.sortedCopyOf( + TEXTURE_COMPRESSION_FORMAT_COMPARATOR, targeting.getAlternativesList())) + .build(); + } + + private TargetingNormalizer() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java index 6351f5ca..e5db1994 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java @@ -16,9 +16,8 @@ package com.android.tools.build.bundletool.model.utils; -import static com.android.bundle.Targeting.ScreenDensity.DensityOneofCase.DENSITY_ALIAS; -import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.DENSITY_ALIAS_TO_DPI_MAP; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; +import static com.google.common.collect.MoreCollectors.onlyElement; import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.ApkTargeting; @@ -30,7 +29,6 @@ import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.VariantTargeting; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.MoreCollectors; import com.google.protobuf.Int32Value; import java.util.Optional; @@ -174,10 +172,7 @@ public static Optional getScreenDensityDpi( ScreenDensity densityTargeting = screenDensityTargeting.getValueList().stream() // For now we only support one value in ScreenDensityTargeting. - .collect(MoreCollectors.onlyElement()); - return Optional.of( - densityTargeting.getDensityOneofCase().equals(DENSITY_ALIAS) - ? DENSITY_ALIAS_TO_DPI_MAP.get(densityTargeting.getDensityAlias()) - : densityTargeting.getDensityDpi()); + .collect(onlyElement()); + return Optional.of(ResourcesUtils.convertToDpi(densityTargeting)); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TextureCompressionUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TextureCompressionUtils.java index f3ac2b84..76d5b958 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TextureCompressionUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TextureCompressionUtils.java @@ -75,6 +75,13 @@ public static Optional textureCompressionFormat( TEXTURE_COMPRESSION_FORMAT_TO_MANIFEST_VALUE.inverse().get(glExtension)); } + private static TextureCompressionFormatTargeting textureCompressionFormat( + TextureCompressionFormatAlias alias) { + return TextureCompressionFormatTargeting.newBuilder() + .addValue(TextureCompressionFormat.newBuilder().setAlias(alias)) + .build(); + } + /** Return the texture compression formats supported by the given OpenGL version. */ public static ImmutableList textureCompressionFormatsForGl( int glVersion) { @@ -87,13 +94,6 @@ public static ImmutableList textureCompressionFor return ImmutableList.of(); } - private static TextureCompressionFormatTargeting textureCompressionFormat( - TextureCompressionFormatAlias alias) { - return TextureCompressionFormatTargeting.newBuilder() - .addValue(TextureCompressionFormat.newBuilder().setAlias(alias)) - .build(); - } - // Do not instantiate. private TextureCompressionUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditions.java b/src/main/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditions.java index d3a7bbc0..2286d85c 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditions.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditions.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; +import com.android.tools.build.bundletool.model.ZipPath; import java.nio.file.Files; import java.nio.file.Path; @@ -42,6 +43,16 @@ public static void checkFileExistsAndExecutable(Path path) { checkArgument(Files.isExecutable(path), "File '%s' is not executable.", path); } + /** + * Checks the extension of the given file. + * + * @param fileDescription description of the file to be used in an error message (eg. "Zip file", + * "APK") + */ + public static void checkFileHasExtension(String fileDescription, ZipPath path, String extension) { + checkFileHasExtension(fileDescription, path.getFileName().toString(), extension); + } + /** * Checks the extension of the given file. * @@ -49,7 +60,11 @@ public static void checkFileExistsAndExecutable(Path path) { * "APK") */ public static void checkFileHasExtension(String fileDescription, Path path, String extension) { - String filename = path.getFileName().toString(); + checkFileHasExtension(fileDescription, path.getFileName().toString(), extension); + } + + private static void checkFileHasExtension( + String fileDescription, String filename, String extension) { checkArgument( filename.endsWith(extension), "%s '%s' is expected to have '%s' extension.", diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/files/FileUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/files/FileUtils.java index 08b473a6..624db3d8 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/files/FileUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/files/FileUtils.java @@ -52,8 +52,11 @@ public static ImmutableList getDistinctParentPaths(Collection paths) } /** Gets the extension of the path file. */ - public static String getFileExtension(Path path) { - Path name = path.getFileName(); + public static String getFileExtension(ZipPath path) { + if (path.getNameCount() == 0) { + return ""; + } + ZipPath name = path.getFileName(); // null for empty paths and root-only paths if (name == null) { 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 226d9708..a419efa5 100755 --- 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.11.0"; + private static final String CURRENT_VERSION = "0.12.0"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { @@ -43,4 +43,6 @@ public static Version getVersionFromBundleConfig(BundleConfig bundleConfig) { return Version.of(rawVersion); } + + private BundleToolVersion() {} } 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 142f88c2..c65fe57f 100755 --- 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 @@ -17,10 +17,49 @@ /** Features that are enabled only from a certain Bundletool version. */ public enum VersionGuardedFeature { + + /** ABI sanitizer no longer applied. */ + ABI_SANITIZER_DISABLED("0.3.1"), + + /** The namespace on the "include" attribute in the AndroidManifest.xml is required. */ + NAMESPACE_ON_INCLUDE_ATTRIBUTE_REQUIRED("0.3.4"), + + /** + * Resources that don't have dpi-alternatives can live in the master split instead of being + * duplicated in each DPI-specific APK. + */ + RESOURCES_WITH_NO_ALTERNATIVES_IN_MASTER_SPLIT("0.4.0"), + + /** The module title is now required. */ + MODULE_TITLE_VALIDATION_ENFORCED("0.4.3"), + + + /** + * No longer keep a list of file extensions that should remain uncompressed by default. This is + * left to the build system to decide. + */ + NO_DEFAULT_UNCOMPRESS_EXTENSIONS("0.7.3"), + + /** + * Move the resources referenced in the AndroidManifest.xml into the master split to ensure + * Application.create() will be invoked even if other resources are missing. Added for the + * Sideloading API. + */ + RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT("0.8.1"), + + /** + * Resources under "drawable-mdpi" take precedence over ones under "drawable". Although they + * technically target the same screen density, developers are confused when we pick the other one. + */ + PREFER_EXPLICIT_DPI_OVER_DEFAULT_CONFIG("0.9.1"), + + /** A new tag replaces the now deprecated "onDemand" attribute. */ + NEW_DELIVERY_TYPE_MANIFEST_TAG("0.10.2"), + /** - * When an APK has minSdkVersion>=24, we should be able to sign only with v2 signing, since the v2 - * signing scheme was introduced in Android N. This reduces the size of apps by removing a few - * files under META-INF. + * When an APK has minSdkVersion>=24, signing the APK only with v2 signing, since the v2 signing + * scheme was introduced in Android N. This reduces the size of apps by removing a few files under + * META-INF. */ NO_V1_SIGNING_WHEN_POSSIBLE("0.11.0"); @@ -31,8 +70,12 @@ public enum VersionGuardedFeature { this.enabledSinceVersion = Version.of(enabledSinceVersion); } - /** Whether the feature should be enabled if the bundle was built with the given version. */ + /** + * Whether the feature should be enabled if the bundle was built with the given version. + * + * @param bundletoolVersion The version of bundletool that was used to build the App Bundle. + */ public boolean enabledForVersion(Version bundletoolVersion) { - return !enabledSinceVersion.isNewerThan(bundletoolVersion); + return !bundletoolVersion.isOlderThan(enabledSinceVersion); } } diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundleObfuscationPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundleObfuscationPreprocessor.java index 8034014d..a60952ee 100755 --- a/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundleObfuscationPreprocessor.java +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundleObfuscationPreprocessor.java @@ -31,6 +31,7 @@ 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.utils.files.FileUtils; import com.android.tools.build.bundletool.validation.ResourceTableValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; @@ -39,7 +40,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; -import com.google.common.io.MoreFiles; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -77,7 +77,7 @@ public AppBundle preprocess(AppBundle originalAppBundle) { } return newAppBundle.setRawModules(obfuscatedBundleModules.build()).build(); } - + private static ResourceTable obfuscateResourceTableEntries( ResourceTable initialResourceTable, ImmutableMap resourceNameMapping) { ResourceTable.Builder modifiedResourceTable = initialResourceTable.toBuilder(); @@ -169,7 +169,7 @@ private static ZipPath obfuscateZipPath( while (resourceNameMapping.containsValue("res/" + encodedString)) { encodedString = handleCollision(hashCode.asBytes()); } - String fileExtension = MoreFiles.getFileExtension(oldZipPath); + String fileExtension = FileUtils.getFileExtension(oldZipPath); // The "xml" extension has to be preserved, because the Android Platform requires it if (Ascii.equalsIgnoreCase(fileExtension, "xml")) { encodedString = encodedString + "." + fileExtension; diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java index 6c8fc04c..9c297f17 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java @@ -27,6 +27,7 @@ import com.android.tools.build.bundletool.model.BundleModule.ModuleDeliveryType; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.SuffixManager; import com.google.common.collect.ImmutableList; import com.google.protobuf.Int32Value; @@ -34,6 +35,7 @@ public class AssetModuleSplitter { private final BundleModule module; private final ApkGenerationConfiguration apkGenerationConfiguration; + private final SuffixManager suffixManager = new SuffixManager(); public AssetModuleSplitter( BundleModule module, ApkGenerationConfiguration apkGenerationConfiguration) { @@ -53,7 +55,7 @@ public ImmutableList splitModule() { splits = splits.stream().map(AssetModuleSplitter::addLPlusApkTargeting).collect(toImmutableList()); } - return splits; + return splits.stream().map(this::setAssetSliceManifest).collect(toImmutableList()); } private SplittingPipeline createAssetsSplittingPipeline() { @@ -81,4 +83,9 @@ private static ModuleSplit addLPlusApkTargeting(ModuleSplit split) { .build()) .build(); } + + private ModuleSplit setAssetSliceManifest(ModuleSplit assetSlice) { + String resolvedSuffix = suffixManager.createSuffix(assetSlice); + return assetSlice.writeSplitIdInManifest(resolvedSuffix); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java index b4b8b394..20621075 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java @@ -23,10 +23,11 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.ModuleDeliveryType; import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionException.VersionCodeMissingException; import com.google.common.collect.ImmutableList; /** - * Generates asset slices from remote asset modules. + * Generates asset slices from asset modules. * *

    Each asset in the module is inserted in at most one asset slice, according to its target. */ @@ -43,7 +44,12 @@ public AssetSlicesGenerator( public ImmutableList generateAssetSlices() { ImmutableList.Builder splits = ImmutableList.builder(); - int versionCode = appBundle.getBaseModule().getAndroidManifest().getVersionCode(); + int versionCode = + appBundle + .getBaseModule() + .getAndroidManifest() + .getVersionCode() + .orElseThrow(VersionCodeMissingException::new); for (BundleModule module : appBundle.getAssetModules().values()) { AssetModuleSplitter moduleSplitter = diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AssetsDimensionSplitterFactory.java b/src/main/java/com/android/tools/build/bundletool/splitters/AssetsDimensionSplitterFactory.java index de3460c0..252ad942 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AssetsDimensionSplitterFactory.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AssetsDimensionSplitterFactory.java @@ -17,7 +17,9 @@ package com.android.tools.build.bundletool.splitters; import static com.android.tools.build.bundletool.model.ManifestMutator.withSplitsRequired; +import static com.android.utils.ImmutableCollectors.toImmutableSet; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Predicates.not; import static com.google.common.collect.ImmutableList.toImmutableList; import com.android.bundle.Files.Assets; @@ -33,6 +35,7 @@ import com.google.common.base.Function; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.protobuf.Message; @@ -108,7 +111,9 @@ public ImmutableCollection split(ModuleSplit split) { .map(assetsConfig -> splitAssetsDirectories(assetsConfig, split)) .orElse(ImmutableList.of(split)) .stream() - .map(moduleSplit -> removeAssetsTargeting(moduleSplit)) + .map( + moduleSplit -> + moduleSplit.isMasterSplit() ? moduleSplit : removeAssetsTargeting(moduleSplit)) .collect(toImmutableList()); } @@ -120,29 +125,54 @@ private ModuleSplit removeAssetsTargeting(ModuleSplit split) { private ImmutableList splitAssetsDirectories(Assets assets, ModuleSplit split) { Multimap directoriesMap = - Multimaps.index( - assets.getDirectoryList(), - targetedDirectory -> dimensionGetter.apply(targetedDirectory.getTargeting())); - return directoriesMap.asMap().entrySet().stream() - .map( + Multimaps.filterKeys( + Multimaps.index( + assets.getDirectoryList(), + targetedDirectory -> dimensionGetter.apply(targetedDirectory.getTargeting())), + not(this::isDefaultTargeting)); + ImmutableList.Builder splitsBuilder = new ImmutableList.Builder<>(); + // Generate config splits. + directoriesMap + .asMap() + .entrySet() + .forEach( entry -> { + ImmutableList entries = + listEntriesFromDirectories(entry.getValue(), split); + if (entries.isEmpty()) { + return; + } ModuleSplit.Builder modifiedSplit = split.toBuilder(); - boolean isMasterSplit = - split.isMasterSplit() && isDefaultTargeting(entry.getKey()); - modifiedSplit - .setEntries(listEntriesFromDirectories(entry.getValue(), split)) + .setEntries(entries) .setApkTargeting(generateTargeting(split.getApkTargeting(), entry.getKey())) - .setMasterSplit(isMasterSplit); - if (!isMasterSplit) { - modifiedSplit.addMasterManifestMutator(withSplitsRequired(true)); - } + .setMasterSplit(false) + .addMasterManifestMutator(withSplitsRequired(true)); + + splitsBuilder.add(modifiedSplit.build()); + }); + // Ensure that master split (even an empty one) always exists. + ModuleSplit defaultSplit = getDefaultAssetsSplit(split, splitsBuilder.build()); + if (defaultSplit.isMasterSplit() || !defaultSplit.getEntries().isEmpty()) { + splitsBuilder.add(defaultSplit); + } + return splitsBuilder.build(); + } - return modifiedSplit.build(); - }) - .filter(moduleSplit -> !moduleSplit.getEntries().isEmpty()) - .collect(toImmutableList()); + private ModuleSplit getDefaultAssetsSplit( + ModuleSplit inputSplit, ImmutableList configSplits) { + ImmutableSet claimedEntries = + configSplits.stream() + .map(ModuleSplit::getEntries) + .flatMap(Collection::stream) + .collect(toImmutableSet()); + return inputSplit.toBuilder() + .setEntries( + inputSplit.getEntries().stream() + .filter(not(claimedEntries::contains)) + .collect(toImmutableList())) + .build(); } private boolean isDefaultTargeting(T splittingDimensionTargeting) { 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 8de1f2a1..cf30f5d1 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java @@ -18,7 +18,6 @@ import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.abiUniverse; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.densityUniverse; -import static com.android.tools.build.bundletool.model.utils.TextureCompressionUtils.TEXTURE_TO_TARGETING; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Predicates.not; @@ -232,19 +231,12 @@ private static ModuleSplit applySuffixStripping( split = TargetingUtils.removeAssetsTargeting(split, dimension); // Apply the updated targeting to the module split (as it now only contains assets for - // the selected TCF), both for the APK and the variant targeting. - return split.toBuilder() - .setApkTargeting( - split.getApkTargeting().toBuilder() - .setTextureCompressionFormatTargeting( - TEXTURE_TO_TARGETING.get(suffixStripping.getDefaultSuffix())) - .build()) - .setVariantTargeting( - split.getVariantTargeting().toBuilder(). - setTextureCompressionFormatTargeting( - TEXTURE_TO_TARGETING.get(suffixStripping.getDefaultSuffix())) - .build()) - .build(); + // the selected TCF) + split = + TargetingUtils.setTargetingByDefaultSuffix( + split, dimension, suffixStripping.getDefaultSuffix()); + + return split; } private SplittingPipeline createNativeLibrariesSplittingPipeline( diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ResourceAnalyzer.java b/src/main/java/com/android/tools/build/bundletool/splitters/ResourceAnalyzer.java index 1aba2d4f..c29f90cb 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ResourceAnalyzer.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ResourceAnalyzer.java @@ -33,9 +33,11 @@ import com.android.tools.build.bundletool.model.ResourceId; import com.android.tools.build.bundletool.model.ResourceTableEntry; import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -140,6 +142,11 @@ private ImmutableSet findAllReferencedAppResources(Item item, Bundle try (InputStream is = module.getEntry(xmlResourcePath).get().getContent()) { XmlNode xmlRoot = XmlNode.parseFrom(is); return findAllReferencedAppResources(xmlRoot, module); + } catch (InvalidProtocolBufferException e) { + throw ValidationException.builder() + .withMessage("Error parsing XML file '%s'.", xmlResourcePath) + .withCause(e) + .build(); } catch (IOException e) { throw new UncheckedIOException( String.format( 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 8bfbc737..2414dbf9 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java @@ -20,6 +20,7 @@ import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.DEFAULT_DENSITY_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MIPMAP_TYPE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.getLowestDensity; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_WITH_NO_ALTERNATIVES_IN_MASTER_SPLIT; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -113,12 +114,9 @@ public ImmutableCollection splitInternal(ModuleSplit split) { continue; } ModuleSplit.Builder moduleSplitBuilder = - split - .toBuilder() + split.toBuilder() .setApkTargeting( - split - .getApkTargeting() - .toBuilder() + split.getApkTargeting().toBuilder() .setScreenDensityTargeting( ScreenDensityTargeting.newBuilder() .addValue(toScreenDensity(density)) @@ -147,8 +145,7 @@ private ModuleSplit getDefaultResourcesSplit( ModuleSplit inputSplit, ImmutableCollection densitySplits) { ResourceTable defaultSplitTable = getResourceTableForDefaultSplit(inputSplit, getClaimedConfigs(densitySplits)); - return inputSplit - .toBuilder() + return inputSplit.toBuilder() .setEntries(ModuleSplit.filterResourceEntries(inputSplit.getEntries(), defaultSplitTable)) .setResourceTable(defaultSplitTable) .build(); @@ -228,16 +225,15 @@ private Entry filterEntryForDensity(ResourceTableEntry tableEntry, DensityAlias // Groups together configs that only differ on density. Map> configValuesByConfiguration = initialEntry.getConfigValueList().stream() - // Remove this filter entirely once 0.4.0 is no longer being actively used. .filter( configValue -> - !bundleVersion.isOlderThan(Version.of("0.4.0")) + RESOURCES_WITH_NO_ALTERNATIVES_IN_MASTER_SPLIT.enabledForVersion(bundleVersion) || configValue.getConfig().getDensity() != DEFAULT_DENSITY_VALUE) .collect(groupingBy(configValue -> clearDensity(configValue.getConfig()))); // Filter out configs that don't have alternatives on density. These configurations can go in // the master split. - if (!bundleVersion.isOlderThan(Version.of("0.4.0"))) { + if (RESOURCES_WITH_NO_ALTERNATIVES_IN_MASTER_SPLIT.enabledForVersion(bundleVersion)) { configValuesByConfiguration = Maps.filterValues(configValuesByConfiguration, configValues -> configValues.size() > 1); } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java index e004e263..9ae99ffc 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java @@ -59,7 +59,7 @@ private static ImmutableSet getSupportedAbis(BundleModule module) { return module .findEntriesUnderPath(BundleModule.LIB_DIRECTORY) // From "lib/

    /..." extract the "" part. - .map(entry -> entry.getPath().getName(1)) + .map(entry -> entry.getPath().getName(1).toString()) // Extract ABI from the directory name. .map(TargetedDirectorySegment::parse) .map(TargetedDirectorySegment::getName) diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java index e7c77dfe..2ee9278f 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java @@ -41,6 +41,7 @@ import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestSdkTargetingException.MinSdkGreaterThanMaxSdkException; import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestSdkTargetingException.MinSdkInvalidException; import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionCodeConflictException; +import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionException.VersionCodeMissingException; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -69,6 +70,7 @@ public void validateSameVersionCode(ImmutableList modules) { .map(BundleModule::getAndroidManifest) .filter(manifest -> !manifest.getModuleType().equals(ModuleType.ASSET_MODULE)) .map(AndroidManifest::getVersionCode) + .map(optVersionCode -> optVersionCode.orElseThrow(VersionCodeMissingException::new)) .distinct() .sorted() .collect(toImmutableList()); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java index 5d253706..1eeb6a26 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java @@ -22,10 +22,13 @@ import static com.android.tools.build.bundletool.model.AbiName.X86_64; import static com.android.tools.build.bundletool.model.BundleModule.ABI_SPLITTER; import static com.android.tools.build.bundletool.model.BundleModule.APEX_DIRECTORY; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_MANIFEST_JSON_PATH; import static com.android.tools.build.bundletool.model.BundleModule.APEX_MANIFEST_PATH; import static com.android.tools.build.bundletool.model.BundleModule.APEX_NOTICE_PATH; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_PUBKEY_PATH; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.android.apex.ApexManifestProto.ApexManifest; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.TargetedApexImage; import com.android.tools.build.bundletool.model.AbiName; @@ -38,8 +41,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -50,7 +56,8 @@ public class ApexBundleValidator extends SubValidator { private static final ImmutableList ALLOWED_APEX_FILES_OUTSIDE_APEX_DIRECTORY = - ImmutableList.of(APEX_MANIFEST_PATH, APEX_NOTICE_PATH); + ImmutableList.of( + APEX_MANIFEST_PATH, APEX_MANIFEST_JSON_PATH, APEX_NOTICE_PATH, APEX_PUBKEY_PATH); // The bundle must contain a system image for at least one of each of these sets. private static final ImmutableSet>> REQUIRED_ONE_OF_ABI_SETS = @@ -90,12 +97,19 @@ public void validateModule(BundleModule module) { } Optional apexManifest = module.getEntry(APEX_MANIFEST_PATH); - if (!apexManifest.isPresent()) { - throw ValidationException.builder() - .withMessage("Missing expected file in APEX bundle: '%s'.", APEX_MANIFEST_PATH) - .build(); + if (apexManifest.isPresent()) { + validateApexManifest(apexManifest.get()); + } else { + apexManifest = module.getEntry(APEX_MANIFEST_JSON_PATH); + if (!apexManifest.isPresent()) { + throw ValidationException.builder() + .withMessage( + "Missing expected file in APEX bundle: '%s' or '%s'.", + APEX_MANIFEST_PATH, APEX_MANIFEST_JSON_PATH) + .build(); + } + validateApexManifestJson(apexManifest.get()); } - validateApexManifest(apexManifest.get()); ImmutableSet.Builder apexImagesBuilder = ImmutableSet.builder(); ImmutableSet.Builder apexFileNamesBuilder = ImmutableSet.builder(); @@ -170,10 +184,30 @@ private static void validateTargeting(ImmutableSet allImages, ApexImages } private static void validateApexManifest(ModuleEntry entry) { + try (InputStream inputStream = entry.getContent()) { + ApexManifest apexManifest = + ApexManifest.parseFrom(inputStream, ExtensionRegistry.getEmptyRegistry()); + if (apexManifest.getName().isEmpty()) { + throw ValidationException.builder() + .withMessage("APEX manifest must have a package name.") + .build(); + } + } catch (InvalidProtocolBufferException e) { + throw ValidationException.builder() + .withMessage("Couldn't parse APEX manifest") + .withCause(e) + .build(); + } catch (IOException e) { + throw new UncheckedIOException("Couldn't read APEX manifest.", e); + } + } + + private static void validateApexManifestJson(ModuleEntry entry) { try (InputStream inputStream = entry.getContent(); BufferedReader reader = BufferedIo.reader(inputStream)) { JsonObject json = new JsonParser().parse(reader).getAsJsonObject(); - if (json.get("name") == null) { + JsonElement element = json.get("name"); + if (element == null || element.getAsString().isEmpty()) { throw ValidationException.builder() .withMessage("APEX manifest must have a package name.") .build(); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java index fb898272..c40ca3af 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java @@ -27,14 +27,14 @@ import com.android.tools.build.bundletool.model.ResourceId; import com.android.tools.build.bundletool.model.ResourceTableEntry; import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.utils.PathMatcher; +import com.android.tools.build.bundletool.model.utils.PathMatcher.GlobPatternSyntaxException; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.android.tools.build.bundletool.model.utils.TextureCompressionUtils; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -59,8 +59,6 @@ public void validateBundle(AppBundle bundle) { } private void validateCompression(Compression compression) { - FileSystem fileSystem = FileSystems.getDefault(); - for (String pattern : compression.getUncompressedGlobList()) { if (FORBIDDEN_CHARS_IN_GLOB.stream().anyMatch(pattern::contains)) { throw ValidationException.builder() @@ -69,8 +67,8 @@ private void validateCompression(Compression compression) { } try { - fileSystem.getPathMatcher("glob:" + pattern); - } catch (IllegalArgumentException e) { + PathMatcher.createFromGlob(pattern); + } catch (GlobPatternSyntaxException e) { throw ValidationException.builder() .withCause(e) .withMessage("Invalid uncompressed glob: '%s'.", pattern) @@ -85,8 +83,7 @@ private void validateOptimizations(Optimizations optimizations) { // We only throw if an unrecognized dimension is enabled, since that would generate an // unexpected output. However, we tolerate if the unknown dimension is negated since the output // will be the same. - if (splitDimensions - .stream() + if (splitDimensions.stream() .anyMatch( dimension -> dimension.getValue().equals(Value.UNRECOGNIZED) && !dimension.getNegate())) { @@ -107,6 +104,7 @@ private void validateOptimizations(Optimizations optimizations) { .anyMatch( dimension -> dimension.hasSuffixStripping() + && dimension.getSuffixStripping().getEnabled() && !dimension.getValue().equals(Value.TEXTURE_COMPRESSION_FORMAT))) { throw ValidationException.builder() .withMessage( @@ -115,17 +113,6 @@ private void validateOptimizations(Optimizations optimizations) { .build(); } - if (splitDimensions.stream() - .anyMatch( - dimension -> - dimension.hasSuffixStripping() - && dimension.getSuffixStripping().getEnabled() - && dimension.getSuffixStripping().getDefaultSuffix().isEmpty())) { - throw ValidationException.builder() - .withMessage("Suffix stripping was enabled without specifying a default suffix.") - .build(); - } - splitDimensions.stream() .filter(dimension -> dimension.getValue().equals(Value.TEXTURE_COMPRESSION_FORMAT)) .filter(dimension -> !dimension.getSuffixStripping().getDefaultSuffix().isEmpty()) diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ModuleTitleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/ModuleTitleValidator.java index 3645aa06..4c7fa952 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/ModuleTitleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ModuleTitleValidator.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.validation; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.entries; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.MODULE_TITLE_VALIDATION_ENFORCED; import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.aapt.Resources.ResourceTable; @@ -43,8 +44,9 @@ private static void checkModuleTitles(ImmutableList modules) { BundleModule baseModule = modules.stream().filter(BundleModule::isBaseModule).findFirst().get(); // For bundles built using older versions we haven't strictly enforced module Title Validation. - if (BundleToolVersion.getVersionFromBundleConfig(baseModule.getBundleConfig()) - .isOlderThan(Version.of("0.4.3"))) { + Version bundleVersion = + BundleToolVersion.getVersionFromBundleConfig(baseModule.getBundleConfig()); + if (!MODULE_TITLE_VALIDATION_ENFORCED.enabledForVersion(bundleVersion)) { return; } ResourceTable table = baseModule.getResourceTable().orElse(ResourceTable.getDefaultInstance()); @@ -60,7 +62,7 @@ private static void checkModuleTitles(ImmutableList modules) { if (module.getAndroidManifest().getTitleRefId().isPresent()) { throw ValidationException.builder() .withMessage( - "Module titles not supported in asset packs, but found in '%s'.", + "Module titles not supported in asset packs, but found in '%s'.", module.getName()) .build(); } diff --git a/src/main/proto/apex_manifest.proto b/src/main/proto/apex_manifest.proto new file mode 100755 index 00000000..1b6474f5 --- /dev/null +++ b/src/main/proto/apex_manifest.proto @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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. + */ + +// This file should be in sync with +// https://android.googlesource.com/platform/system/apex/+/refs/heads/master/proto/apex_manifest.proto +// But, since bundletool itself doesn't need to read other than name, +// let's keep this as minimal. + +syntax = "proto3"; + +package apex.proto; + +option java_package = "com.android.apex"; +option java_outer_classname = "ApexManifestProto"; + +message ApexManifest { + + // Package Name + string name = 1; + +} diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index 1f4176fe..c323e217 100755 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -9,6 +9,9 @@ option java_package = "com.android.bundle"; // Describes the output of the "build-apks" command. message BuildApksResult { + // The package name of this app. + string package_name = 4; + // List of the created variants. repeated Variant variant = 1; diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index b338e664..1a1332dc 100755 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -101,5 +101,10 @@ message SuffixStripping { // default suffix defines the directories to retain. The others are // discarded: standalone/universal APKs will contain only directories // targeted at this value for the dimension. + // + // If not set or empty, the fallback directory in each directory group will be + // used (for example, if both "assets/level1_textures#tcf_etc1" and + // "assets/level1_textures" are present and the default suffix is empty, + // then only "assets/level1_textures" will be used). string default_suffix = 2; } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java index d0d58be7..864af91f 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -24,6 +24,7 @@ import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_SERIAL; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; import static com.google.common.base.StandardSystemProperty.USER_HOME; @@ -32,16 +33,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import com.android.bundle.Commands.BuildApksResult; import com.android.tools.build.bundletool.device.AdbServer; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.flags.FlagParser.FlagParseException; +import com.android.tools.build.bundletool.io.AppBundleSerializer; import com.android.tools.build.bundletool.model.Aapt2Command; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.SigningConfiguration; 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.ResultUtils; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.android.tools.build.bundletool.testing.Aapt2Helper; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.CertificateFactory; import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; import com.google.common.collect.ImmutableList; @@ -49,6 +55,7 @@ import com.google.common.collect.ImmutableSet; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.PrintStream; import java.nio.file.Path; import java.nio.file.Paths; @@ -617,6 +624,27 @@ public void keystoreProvidedDoesNotPrintWarning() throws Exception { .doesNotContain("WARNING: The APKs won't be signed"); } + @Test + public void packageNameIsPropagatedToBuildResult() throws IOException { + Path testBundlePath = tmpDir.resolve("bundle"); + Path testOutputFile = tmpDir.resolve("app.apks"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule("base", module -> module.setManifest(androidManifest("com.app"))) + .build(); + new AppBundleSerializer().writeToDisk(appBundle, testBundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(testBundlePath) + .setOutputFile(testOutputFile) + .build(); + + Path apksArchive = command.execute(); + BuildApksResult result = ResultUtils.readTableOfContents(apksArchive); + assertThat(result.getPackageName()).isEqualTo("com.app"); + } + private static void createDebugKeystore(Path path) throws Exception { KeyPair keyPair = KeyPairGenerator.getInstance("RSA").genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); 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 85fdbae6..c937f433 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -18,6 +18,7 @@ import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.MIPS; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ATC; @@ -113,6 +114,7 @@ import com.android.aapt.ConfigurationOuterClass.Configuration; import com.android.aapt.Resources.XmlNode; +import com.android.apex.ApexManifestProto.ApexManifest; import com.android.apksig.ApkVerifier; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; @@ -160,6 +162,7 @@ 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.ValidationException; +import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionException.VersionCodeMissingException; import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; @@ -177,6 +180,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; @@ -231,8 +235,9 @@ public class BuildApksManagerTest { variantSdkTargeting(/* minSdkVersion= */ 1); private static final SdkVersion LOWEST_SDK_VERSION = sdkVersionFrom(1); - private static final String APEX_MANIFEST_PATH = "root/apex_manifest.json"; - private static final byte[] APEX_MANIFEST = "{\"name\": \"com.test.app\"}".getBytes(UTF_8); + private static final String APEX_MANIFEST_PATH = "root/apex_manifest.pb"; + private static final byte[] APEX_MANIFEST = + ApexManifest.newBuilder().setName("com.test.app").build().toByteArray(); @Rule public final TemporaryFolder tmp = new TemporaryFolder(); @@ -707,6 +712,103 @@ public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits( ZipFile apkSetFile = openZipFile(apkSetFilePath.toFile()); BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(systemApkVariants(result)).hasSize(1); + Variant systemVariant = result.getVariant(0); + assertThat(systemVariant.getVariantNumber()).isEqualTo(0); + assertThat(systemVariant.getApkSetList()).hasSize(1); + ApkSet baseApkSet = Iterables.getOnlyElement(systemVariant.getApkSetList()); + assertThat(baseApkSet.getModuleMetadata().getName()).isEqualTo("base"); + if (systemApkBuildMode.equals(SYSTEM)) { + // Single System APK. + assertThat(baseApkSet.getApkDescriptionList()).hasSize(2); + assertThat( + baseApkSet.getApkDescriptionList().stream() + .map(ApkDescription::getPath) + .collect(toImmutableSet())) + .containsExactly("system/system.apk", "splits/base-fr.apk"); + } else if (systemApkBuildMode.equals(SYSTEM_COMPRESSED)) { + // Stub and Compressed APK. + assertThat(baseApkSet.getApkDescriptionList()).hasSize(3); + assertThat( + baseApkSet.getApkDescriptionList().stream() + .map(ApkDescription::getPath) + .collect(toImmutableSet())) + .containsExactly("system/system.apk", "system/system.apk.gz", "splits/base-fr.apk"); + } else { + throw new RuntimeException("Unknown APK build mode"); + } + baseApkSet + .getApkDescriptionList() + .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); + } + + @Test + @Theory + public void multipleModulesFusedAndNotFused_systemApks_hasCorrectAdditionalLanguageSplits( + @FromDataPoints("systemApkBuildModes") ApkBuildMode systemApkBuildMode) throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/base.txt") + .setManifest(androidManifest("com.app")) + .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .addModule( + "fused", + module -> + module + .addFile("assets/fused.txt") + .addFile("assets/fused/languages#lang_es/image.jpg") + .addFile("assets/fused/languages#lang_fr/image.jpg") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/fused/languages#lang_es", + assetsDirectoryTargeting(languageTargeting("es"))), + targetedAssetsDirectory( + "assets/fused/languages#lang_fr", + assetsDirectoryTargeting(languageTargeting("fr"))))) + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(true), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .addModule( + "notfused", + module -> + module + .addFile("assets/notfused.txt") + .addFile("assets/notfused/languages#lang_it/image.jpg") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/notfused/languages#lang_it", + assetsDirectoryTargeting(languageTargeting("it"))))) + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(false), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .build(); + Path bundlePath = createAndStoreBundle(appBundle); + + Path apkSetFilePath = + execute( + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .setApkBuildMode(systemApkBuildMode) + .setDeviceSpec( + mergeSpecs( + sdkVersion(28), abis("x86"), density(DensityAlias.MDPI), locales("es"))) + .build()); + + ZipFile apkSetFile = openZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(systemApkVariants(result)).hasSize(1); Variant systemVariant = result.getVariant(0); assertThat(systemVariant.getVariantNumber()).isEqualTo(0); @@ -714,7 +816,7 @@ public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits( ImmutableMap apkSetByModule = Maps.uniqueIndex( systemVariant.getApkSetList(), apkSet -> apkSet.getModuleMetadata().getName()); - assertThat(apkSetByModule.keySet()).containsExactly("base", "fused"); + assertThat(apkSetByModule.keySet()).containsExactly("base", "notfused"); ApkSet baseApkSet = apkSetByModule.get("base"); if (systemApkBuildMode.equals(SYSTEM)) { // Single System APK. @@ -724,7 +826,7 @@ public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits( .map(ApkDescription::getPath) .collect(toImmutableSet())) .containsExactly("system/system.apk", "splits/base-fr.apk"); - } else { + } else if (systemApkBuildMode.equals(SYSTEM_COMPRESSED)) { // Stub and Compressed APK. assertThat(baseApkSet.getApkDescriptionList()).hasSize(3); assertThat( @@ -732,23 +834,26 @@ public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits( .map(ApkDescription::getPath) .collect(toImmutableSet())) .containsExactly("system/system.apk", "system/system.apk.gz", "splits/base-fr.apk"); + } else { + throw new RuntimeException("Unknown APK build mode"); } baseApkSet .getApkDescriptionList() .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); - ApkSet fusedApkSet = apkSetByModule.get("fused"); - assertThat(fusedApkSet.getApkDescriptionList()).hasSize(1); + ApkSet nonFusedApkSet = apkSetByModule.get("notfused"); + assertThat(nonFusedApkSet.getApkDescriptionList()).hasSize(2); assertThat( - fusedApkSet.getApkDescriptionList().stream() + nonFusedApkSet.getApkDescriptionList().stream() .map(ApkDescription::getPath) .collect(toImmutableSet())) - .containsExactly("splits/fused-fr.apk"); - fusedApkSet + .containsExactly("splits/notfused-master.apk", "splits/notfused-it.apk"); + nonFusedApkSet .getApkDescriptionList() .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); } + @Test public void bundleWithDirectoryZipEntries_throws() throws Exception { AppBundle tmpBundle = @@ -1158,10 +1263,9 @@ public void buildApksCommand_universal_generatesSingleApkWithNoOptimizations() t .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) // Add some density-specific resources. .addFile("res/drawable-ldpi/image.jpg") .addFile("res/drawable-mdpi/image.jpg") @@ -1352,10 +1456,9 @@ public void buildApksCommand_compressedSystem_generatesSingleApkWithEmptyOptimiz .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) // Add some density-specific resources. .addFile("res/drawable-ldpi/image.jpg") .addFile("res/drawable-mdpi/image.jpg") @@ -1455,10 +1558,9 @@ public void buildApksCommand_system_generatesSingleApkWithEmptyOptimizations() t .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) // Add some density-specific resources. .addFile("res/drawable-ldpi/image.jpg") .addFile("res/drawable-mdpi/image.jpg") @@ -1660,8 +1762,7 @@ public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { .addFile("lib/x86/libsome.so") .setNativeConfig( nativeLibraries( - targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)))) + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app", withMinSdkVersion(25)))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -1697,8 +1798,7 @@ public void buildApksCommand_splitApks_honorsMaxSdkVersion() throws Exception { .addFile("lib/x86/libsome.so") .setNativeConfig( nativeLibraries( - targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)))) + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app", withMaxSdkVersion(21)))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -1769,12 +1869,11 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception .addFile("lib/mips/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + "lib/x86_64", nativeDirectoryTargeting(X86_64)), targetedNativeDirectory( - "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + "lib/mips", nativeDirectoryTargeting(MIPS)))) .setManifest(androidManifest("com.test.app"))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -1799,32 +1898,32 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception return getOnlyElement(apkDescription.getTargeting().getAbiTargeting().getValueList()); }); assertThat(standaloneVariantsByAbi.keySet()) - .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.X86_64), toAbi(AbiAlias.MIPS)); - assertThat(standaloneVariantsByAbi.get(toAbi(AbiAlias.X86)).getTargeting()) + .containsExactly(toAbi(X86), toAbi(X86_64), toAbi(MIPS)); + assertThat(standaloneVariantsByAbi.get(toAbi(X86)).getTargeting()) .ignoringRepeatedFieldOrder() .isEqualTo( mergeVariantTargeting( - variantAbiTargeting(AbiAlias.X86, ImmutableSet.of(AbiAlias.X86_64, AbiAlias.MIPS)), + variantAbiTargeting(X86, ImmutableSet.of(X86_64, MIPS)), variantSdkTargeting( LOWEST_SDK_VERSION, ImmutableSet.of( sdkVersionFrom(ANDROID_L_API_VERSION), sdkVersionFrom(ANDROID_M_API_VERSION))))); - assertThat(standaloneVariantsByAbi.get(toAbi(AbiAlias.X86_64)).getTargeting()) + assertThat(standaloneVariantsByAbi.get(toAbi(X86_64)).getTargeting()) .ignoringRepeatedFieldOrder() .isEqualTo( mergeVariantTargeting( - variantAbiTargeting(AbiAlias.X86_64, ImmutableSet.of(AbiAlias.X86, AbiAlias.MIPS)), + variantAbiTargeting(X86_64, ImmutableSet.of(X86, MIPS)), variantSdkTargeting( LOWEST_SDK_VERSION, ImmutableSet.of( sdkVersionFrom(ANDROID_L_API_VERSION), sdkVersionFrom(ANDROID_M_API_VERSION))))); - assertThat(standaloneVariantsByAbi.get(toAbi(AbiAlias.MIPS)).getTargeting()) + assertThat(standaloneVariantsByAbi.get(toAbi(MIPS)).getTargeting()) .ignoringRepeatedFieldOrder() .isEqualTo( mergeVariantTargeting( - variantAbiTargeting(AbiAlias.MIPS, ImmutableSet.of(AbiAlias.X86, AbiAlias.X86_64)), + variantAbiTargeting(MIPS, ImmutableSet.of(X86, X86_64)), variantSdkTargeting( LOWEST_SDK_VERSION, ImmutableSet.of( @@ -1854,12 +1953,11 @@ public void buildApksCommand_system_withoutLanguageTargeting( .addFile("lib/mips/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + "lib/x86_64", nativeDirectoryTargeting(X86_64)), targetedNativeDirectory( - "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + "lib/mips", nativeDirectoryTargeting(MIPS)))) .setManifest(androidManifest("com.test.app"))) .setBundleConfig( BundleConfigBuilder.create() @@ -1893,7 +1991,7 @@ public void buildApksCommand_system_withoutLanguageTargeting( assertThat(systemVariant.getTargeting()) .isEqualTo( mergeVariantTargeting( - variantAbiTargeting(AbiAlias.MIPS, ImmutableSet.of(AbiAlias.X86, AbiAlias.X86_64)), + variantAbiTargeting(MIPS, ImmutableSet.of(X86, X86_64)), variantSdkTargeting(LOWEST_SDK_VERSION))); if (systemApkBuildMode.equals(SYSTEM)) { @@ -1944,12 +2042,11 @@ public void buildApksCommand_system_withLanguageTargeting( assetsDirectoryTargeting(languageTargeting("fr"))))) .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + "lib/x86_64", nativeDirectoryTargeting(X86_64)), targetedNativeDirectory( - "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + "lib/mips", nativeDirectoryTargeting(MIPS)))) .setManifest(androidManifest("com.test.app"))) .addModule( "not_fused", @@ -1974,12 +2071,11 @@ public void buildApksCommand_system_withLanguageTargeting( assetsDirectoryTargeting(languageTargeting("fr"))))) .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + "lib/x86_64", nativeDirectoryTargeting(X86_64)), targetedNativeDirectory( - "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + "lib/mips", nativeDirectoryTargeting(MIPS)))) .setManifest( androidManifestForFeature( "com.test.app", @@ -2009,7 +2105,7 @@ public void buildApksCommand_system_withLanguageTargeting( .ignoringRepeatedFieldOrder() .isEqualTo( mergeVariantTargeting( - variantAbiTargeting(AbiAlias.X86, ImmutableSet.of(AbiAlias.X86_64, AbiAlias.MIPS)), + variantAbiTargeting(X86, ImmutableSet.of(X86_64, MIPS)), variantSdkTargeting(LOWEST_SDK_VERSION))); assertThat(x86Variant.getApkSetList()).hasSize(2); @@ -2069,10 +2165,9 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { .addFile("lib/x86_64/libfeature.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) .setManifest( androidManifestForFeature( "com.test.app", @@ -2134,12 +2229,10 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { apkDescriptions(standaloneApkVariants(result)), apkDesc -> getOnlyElement(apkDesc.getTargeting().getAbiTargeting().getValueList())); - assertThat(standaloneApksByAbi.keySet()) - .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.X86_64)); + assertThat(standaloneApksByAbi.keySet()).containsExactly(toAbi(X86), toAbi(X86_64)); File x86ApkFile = - extractFromApkSetFile( - apkSetFile, standaloneApksByAbi.get(toAbi(AbiAlias.X86)).getPath(), outputDir); + extractFromApkSetFile(apkSetFile, standaloneApksByAbi.get(toAbi(X86)).getPath(), outputDir); try (ZipFile x86Zip = new ZipFile(x86ApkFile)) { // ABI-specific files. assertThat(x86Zip).hasFile("lib/x86/libfeature.so"); @@ -2153,7 +2246,7 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { File x64ApkFile = extractFromApkSetFile( - apkSetFile, standaloneApksByAbi.get(toAbi(AbiAlias.X86_64)).getPath(), outputDir); + apkSetFile, standaloneApksByAbi.get(toAbi(X86_64)).getPath(), outputDir); try (ZipFile x64Zip = new ZipFile(x64ApkFile)) { // ABI-specific files. assertThat(x64Zip).hasFile("lib/x86_64/libfeature.so"); @@ -2362,10 +2455,9 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) .setManifest(androidManifest("com.test.app"))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -2388,21 +2480,21 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() VariantTargeting lSplitVariantTargeting = variantSdkTargeting( sdkVersionFrom(ANDROID_L_API_VERSION), - ImmutableSet.of(sdkVersionFrom(ANDROID_M_API_VERSION), LOWEST_SDK_VERSION)); + ImmutableSet.of(LOWEST_SDK_VERSION, sdkVersionFrom(ANDROID_M_API_VERSION))); VariantTargeting mSplitVariantTargeting = variantSdkTargeting( sdkVersionFrom(ANDROID_M_API_VERSION), - ImmutableSet.of(sdkVersionFrom(ANDROID_L_API_VERSION), LOWEST_SDK_VERSION)); + ImmutableSet.of(LOWEST_SDK_VERSION, sdkVersionFrom(ANDROID_L_API_VERSION))); VariantTargeting standaloneX86VariantTargeting = mergeVariantTargeting( - variantAbiTargeting(AbiAlias.X86, ImmutableSet.of(AbiAlias.X86_64)), + variantAbiTargeting(X86, ImmutableSet.of(X86_64)), variantSdkTargeting( LOWEST_SDK_VERSION, ImmutableSet.of( sdkVersionFrom(ANDROID_L_API_VERSION), sdkVersionFrom(ANDROID_M_API_VERSION)))); VariantTargeting standaloneX64VariantTargeting = mergeVariantTargeting( - variantAbiTargeting(AbiAlias.X86_64, ImmutableSet.of(AbiAlias.X86)), + variantAbiTargeting(X86_64, ImmutableSet.of(X86)), variantSdkTargeting( LOWEST_SDK_VERSION, ImmutableSet.of( @@ -2451,12 +2543,11 @@ public void buildApksCommand_inconsistentAbis_discarded() throws Exception { .setManifest(androidManifest("com.app")) .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + "lib/x86_64", nativeDirectoryTargeting(X86_64)), targetedNativeDirectory( - "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS))))) + "lib/mips", nativeDirectoryTargeting(MIPS))))) // The inconsistent ABIs are discarded up until 0.3.0. .setBundleConfig(BundleConfigBuilder.create().setVersion("0.3.0").build()) .build(); @@ -2480,13 +2571,13 @@ public void buildApksCommand_inconsistentAbis_discarded() throws Exception { apkDescriptions(splitApkVariants(result)), apkDesc -> apkDesc.getTargeting().getAbiTargeting()); assertThat(splitApksByAbiTargeting.keySet()) - .containsExactly(AbiTargeting.getDefaultInstance(), abiTargeting(AbiAlias.X86)); + .containsExactly(AbiTargeting.getDefaultInstance(), abiTargeting(X86)); // x86_64- and mips-specific files should be discarded in the standalone APK. ImmutableList standaloneApks = apkDescriptions(standaloneApkVariants(result)); assertThat(standaloneApks).hasSize(1); assertThat(standaloneApks.get(0).getTargeting().getAbiTargeting()) - .isEqualTo(abiTargeting(AbiAlias.X86, ImmutableSet.of())); + .isEqualTo(abiTargeting(X86, ImmutableSet.of())); File standaloneApkFile = extractFromApkSetFile(apkSetFile, standaloneApks.get(0).getPath(), outputDir); try (ZipFile standaloneApkZipFile = new ZipFile(standaloneApkFile)) { @@ -2533,16 +2624,16 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle() throws Except .setOutputFile(outputFilePath) .build()); - ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); - ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64X86Set = ImmutableSet.of(X86, X86_64); + ImmutableSet x64ArmSet = ImmutableSet.of(ARMEABI_V7A, X86_64); ImmutableSet x64Set = ImmutableSet.of(X86_64); - ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86ArmSet = ImmutableSet.of(ARMEABI_V7A, X86); ImmutableSet x86Set = ImmutableSet.of(X86); ImmutableSet arm8Set = ImmutableSet.of(ARM64_V8A); ImmutableSet arm7Set = ImmutableSet.of(ARMEABI_V7A); ImmutableSet> allTargeting = - ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, arm8Set, arm7Set); + ImmutableSet.of(arm7Set, x86ArmSet, x64ArmSet, arm8Set, x86Set, x64X86Set, x64Set); ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); ApkTargeting x64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); @@ -2634,7 +2725,7 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle_previewTargetSd ImmutableSet arm7Set = ImmutableSet.of(ARMEABI_V7A); ImmutableSet> allTargeting = - ImmutableSet.of(x64Set, x86Set, arm8Set, arm7Set); + ImmutableSet.of(arm7Set, arm8Set, x86Set, x64Set); ApkTargeting x64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); ApkTargeting x86Targeting = apkMultiAbiTargetingFromAllTargeting(x86Set, allTargeting); ApkTargeting arm8Targeting = apkMultiAbiTargetingFromAllTargeting(arm8Set, allTargeting); @@ -2706,7 +2797,7 @@ public void buildApksCommand_apkNotificationMessageKeyApexBundle_hasRightSuffix( "standalones/standalone-x86.apex", "standalones/standalone-arm64_v8a.apex", "standalones/standalone-armeabi_v7a.apex", - "standalones/standalone-arm64_v8a.armeabi_v7a.apex"); + "standalones/standalone-armeabi_v7a.arm64_v8a.apex"); } @DataPoints("bundleVersion") @@ -2868,10 +2959,9 @@ private void runSingleConcurrencyTest_disableNativeLibrariesOptimization(int thr .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) .setManifest( androidManifestForFeature( "com.test.app", @@ -3017,8 +3107,7 @@ public void splitFileNames_abi() throws Exception { .setManifest(androidManifest("com.test.app")) .setNativeConfig( nativeLibraries( - targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86))))) + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86))))) .build(); Path bundlePath = createAndStoreBundle(appBundle); @@ -3040,11 +3129,11 @@ public void splitFileNames_abi() throws Exception { VariantTargeting lSplitVariantTargeting = variantSdkTargeting( sdkVersionFrom(ANDROID_L_API_VERSION), - ImmutableSet.of(sdkVersionFrom(ANDROID_M_API_VERSION), LOWEST_SDK_VERSION)); + ImmutableSet.of(LOWEST_SDK_VERSION, sdkVersionFrom(ANDROID_M_API_VERSION))); VariantTargeting mSplitVariantTargeting = variantSdkTargeting( sdkVersionFrom(ANDROID_M_API_VERSION), - ImmutableSet.of(sdkVersionFrom(ANDROID_L_API_VERSION), LOWEST_SDK_VERSION)); + ImmutableSet.of(LOWEST_SDK_VERSION, sdkVersionFrom(ANDROID_L_API_VERSION))); assertThat(splitVariantsByTargeting.keySet()) .containsExactly(lSplitVariantTargeting, mSplitVariantTargeting); @@ -3280,10 +3369,9 @@ public void extractApkSet_outputApksWithoutArchive() throws Exception { .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( - "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), - targetedNativeDirectory( - "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) .setManifest( androidManifestForFeature( "com.test.app", @@ -4216,7 +4304,7 @@ private int extractVersionCode(File apk) { AndroidManifest.create( XmlNode.parseFrom( protoApk.getInputStream(protoApk.getEntry("AndroidManifest.xml")))); - return androidManifest.getVersionCode(); + return androidManifest.getVersionCode().orElseThrow(VersionCodeMissingException::new); } finally { Files.deleteIfExists(protoApkPath); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java index 94e94079..bd9df60c 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -1087,7 +1087,7 @@ public void printHelp_doesNotCrash() { @Test public void extractInstant_withBaseOnly() throws Exception { - Path apkLBase = ZipPath.create("apkL-base.apk"); + ZipPath apkLBase = ZipPath.create("apkL-base.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( @@ -1206,10 +1206,10 @@ public void extractApks_aboveMaxSdk_throws() throws Exception { @Test public void extractInstant_withModulesFlag() throws Exception { - Path apkPreL = ZipPath.create("apkPreL.apk"); - Path apkLBase = ZipPath.create("apkL-base.apk"); - Path apkLFeature = ZipPath.create("apkL-feature.apk"); - Path apkLOther = ZipPath.create("apkL-other.apk"); + ZipPath apkPreL = ZipPath.create("apkPreL.apk"); + ZipPath apkLBase = ZipPath.create("apkL-base.apk"); + ZipPath apkLFeature = ZipPath.create("apkL-feature.apk"); + ZipPath apkLOther = ZipPath.create("apkL-other.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( @@ -1297,9 +1297,9 @@ public void extractInstant_withBaseAndSingleInstantModule() throws Exception { @Test public void extractInstant_withMultipleInstantModule() throws Exception { - Path apkBase = ZipPath.create("apkL-base.apk"); - Path apkInstant = ZipPath.create("apkL-instant.apk"); - Path apkInstant2 = ZipPath.create("apkL-instant2.apk"); + ZipPath apkBase = ZipPath.create("apkL-base.apk"); + ZipPath apkInstant = ZipPath.create("apkL-instant.apk"); + ZipPath apkInstant2 = ZipPath.create("apkL-instant2.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( @@ -1387,7 +1387,7 @@ private Path copyToTempDir(String testDataPath) throws Exception { return outputFile; } - private Path inOutputDirectory(Path file) { + private Path inOutputDirectory(ZipPath file) { return tmpDir.resolve(Paths.get(file.toString())); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java index 205c3210..55a8be72 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java @@ -220,6 +220,7 @@ public void fromFlagsEquivalentToBuilder_androidSerialVariable() throws Exceptio assertThat(fromBuilder).isEqualTo(fromFlags); } + @Test public void fromFlagsEquivalentToBuilder_modules() throws Exception { Path apksFile = tmpDir.resolve("appbundle.apks"); @@ -244,6 +245,7 @@ public void fromFlagsEquivalentToBuilder_modules() throws Exception { assertThat(fromBuilder).isEqualTo(fromFlags); } + @Test public void missingApksFlag_fails() { expectMissingRequiredBuilderPropertyException( @@ -699,6 +701,54 @@ public void moduleDependencies_diamondGraph( "feature4-master.apk"); } + @Test + @Theory + public void installModules_withPush(@FromDataPoints("apksInDirectory") boolean apksInDirectory) + throws Exception { + BuildApksResult tableOfContent = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk"))), + createSplitApkSet( + "base", + createApkDescription( + apkLanguageTargeting("pl"), + ZipPath.create("base-pl.apk"), + /* isMasterSplit= */ false)))) + .build(); + Path apksFile = createApks(tableOfContent, apksInDirectory); + List installedApks = new ArrayList<>(); + List pushedApks = new ArrayList<>(); + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); + AdbServer adbServer = + new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); + fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + fakeDevice.setPushApksSideEffect((apks, installOptions) -> pushedApks.addAll(apks)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .setPushSplitsPath("/tmp/") + .build() + .execute(); + + assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + .containsExactly("base-master.apk"); + + assertThat(Lists.transform(pushedApks, apkPath -> apkPath.getFileName().toString())) + .containsExactly("base-master.apk", "base-pl.apk"); + } + @Test @Theory public void extractAssetModules(@FromDataPoints("apksInDirectory") boolean apksInDirectory) diff --git a/src/test/java/com/android/tools/build/bundletool/device/ApksInstallerTest.java b/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java similarity index 76% rename from src/test/java/com/android/tools/build/bundletool/device/ApksInstallerTest.java rename to src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java index b88fe291..a9fae179 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/ApksInstallerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java @@ -34,7 +34,7 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class ApksInstallerTest { +public class AdbRunnerTest { private static final InstallOptions DEFAULT_INSTALL_OPTIONS = InstallOptions.builder() @@ -48,14 +48,16 @@ public void installApks_noDeviceId_noConnectedDevices_throws() { AdbServer testAdbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of()); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); Throwable exception = assertThrows( CommandExecutionException.class, () -> - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS)); + adbRunner.run( + device -> + device.installApks( + ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS))); assertThat(exception) .hasMessageThat() .contains("Expected to find one connected device, but found none."); @@ -69,9 +71,11 @@ public void installApks_noDeviceId_oneConnectedDevice_ok() { ImmutableList.of( FakeDevice.fromDeviceSpec("a", DeviceState.ONLINE, lDeviceWithLocales("en-US")))); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); - apksInstaller.installApks(ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS); + adbRunner.run( + device -> + device.installApks(ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS)); } @Test @@ -83,14 +87,16 @@ public void installApks_noDeviceId_twoConnectedDevices_throws() { FakeDevice.fromDeviceSpec("a", DeviceState.ONLINE, lDeviceWithLocales("en-US")), FakeDevice.inDisconnectedState("b", DeviceState.UNAUTHORIZED))); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); Throwable exception = assertThrows( CommandExecutionException.class, () -> - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS)); + adbRunner.run( + device -> + device.installApks( + ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS))); assertThat(exception) .hasMessageThat() .contains("Expected to find one connected device, but found 2."); @@ -101,14 +107,17 @@ public void installApks_withDeviceId_noConnectedDevices_throws() { AdbServer testAdbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of()); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); Throwable exception = assertThrows( CommandExecutionException.class, () -> - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS, "device1")); + adbRunner.run( + device -> + device.installApks( + ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + "device1")); assertThat(exception) .hasMessageThat() .contains("Expected to find one connected device with serial number 'device1'."); @@ -124,10 +133,12 @@ public void installApks_withDeviceId_connectedDevices_ok() { "device1", DeviceState.ONLINE, lDeviceWithLocales("en-US")), FakeDevice.inDisconnectedState("device2", DeviceState.UNAUTHORIZED))); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS, "device1"); + adbRunner.run( + device -> + device.installApks(ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + "device1"); } @Test @@ -142,7 +153,7 @@ public void installApks_withDeviceId_disconnectedDevice_throws() { "device1", DeviceState.ONLINE, lDeviceWithLocales("en-US")), disconnectedDevice)); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); disconnectedDevice.setInstallApksSideEffect( (apks, installOptions) -> { @@ -154,8 +165,11 @@ public void installApks_withDeviceId_disconnectedDevice_throws() { assertThrows( InstallationException.class, () -> - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS, "device2")); + adbRunner.run( + device -> + device.installApks( + ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + "device2")); assertThat(exception) .hasMessageThat() .contains("Enable USB debugging on the connected device."); @@ -168,7 +182,7 @@ public void installApks_allowingDowngrade() { AdbServer testAdbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); testAdbServer.init(Paths.get("/test/adb")); - ApksInstaller apksInstaller = new ApksInstaller(testAdbServer); + AdbRunner adbRunner = new AdbRunner(testAdbServer); fakeDevice.setInstallApksSideEffect( (apks, installOptions) -> { @@ -177,8 +191,10 @@ public void installApks_allowingDowngrade() { } }); - apksInstaller.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), - InstallOptions.builder().setAllowDowngrade(true).build()); + adbRunner.run( + device -> + device.installApks( + ImmutableList.of(Paths.get("apkOne.apk")), + InstallOptions.builder().setAllowDowngrade(true).build())); } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java index 83bbae2e..2754b04d 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java @@ -26,6 +26,7 @@ import com.android.ddmlib.IDevice; import com.android.sdklib.AndroidVersion; import com.android.sdklib.AndroidVersion.VersionCodes; +import com.android.tools.build.bundletool.device.DdmlibDevice.RemoteCommandExecutor; import com.android.tools.build.bundletool.device.Device.InstallOptions; import com.google.common.collect.ImmutableList; import java.nio.file.Path; @@ -95,4 +96,39 @@ public void allowDowngrade_postL() throws Exception { assertThat(extraArgsCaptor.getValue()).contains("-d"); } + + @Test + public void allowTestOnly() throws Exception { + when(mockDevice.getVersion()).thenReturn(new AndroidVersion(VersionCodes.KITKAT)); + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + + ddmlibDevice.installApks( + ImmutableList.of(APK_PATH), InstallOptions.builder().setAllowTestOnly(true).build()); + + verify(mockDevice).installPackage(eq(APK_PATH.toString()), anyBoolean(), eq("-t")); + } + + @Test + public void joinUnixPathsTest() { + assertThat(DdmlibDevice.joinUnixPaths("/", "splits", "mysplits")).isEqualTo("/splits/mysplits"); + assertThat(DdmlibDevice.joinUnixPaths("splits", "mysplits")).isEqualTo("splits/mysplits"); + assertThat(DdmlibDevice.joinUnixPaths("/", "splits/", "mysplits")) + .isEqualTo("/splits/mysplits"); + assertThat(DdmlibDevice.joinUnixPaths("/", "splits", "mysplits/")) + .isEqualTo("/splits/mysplits/"); + } + + @Test + public void escapeAndSingleQuoteTest() { + assertThat(RemoteCommandExecutor.escapeAndSingleQuote("abc")).isEqualTo("'abc'"); + assertThat(RemoteCommandExecutor.escapeAndSingleQuote("ab'c")).isEqualTo("'ab'\\''c'"); + } + + @Test + public void formatCommandWithArgsTest() { + assertThat(RemoteCommandExecutor.formatCommandWithArgs("cat %s %s", "abc", "def")) + .isEqualTo("cat 'abc' 'def'"); + assertThat(RemoteCommandExecutor.formatCommandWithArgs("cat %s %s", "ab'c", "de'f")) + .isEqualTo("cat 'ab'\\''c' 'de'\\''f'"); + } } 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 53d1cf69..f0ac55ef 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java @@ -69,6 +69,7 @@ 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; @@ -78,7 +79,7 @@ public class VariantTotalSizeAggregatorTest { private final GetSizeCommand.Builder getSizeCommand = GetSizeCommand.builder() - .setApksArchivePath(ZipPath.create("dummy.apks")) + .setApksArchivePath(Paths.get("dummy.apks")) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL); @Test diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java index 1c67bfed..c3848a6c 100755 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java @@ -16,6 +16,9 @@ package com.android.tools.build.bundletool.mergers; +import static com.android.bundle.Targeting.Abi.AbiAlias.MIPS; +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.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.locales; import static com.android.tools.build.bundletool.testing.DeviceFactory.mergeSpecs; @@ -59,7 +62,6 @@ import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; import com.android.bundle.Files.TargetedAssetsDirectory; -import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.tools.build.bundletool.TestData; @@ -116,7 +118,7 @@ public void merge_oneSetOfSplits_producesSingleShard() throws Exception { .setEntries( ImmutableList.of(InMemoryModuleEntry.ofFile("lib/x86/libtest.so", DUMMY_CONTENT))) .setMasterSplit(false) - .setApkTargeting(apkAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkAbiTargeting(X86)) .build(); ImmutableList shards = @@ -124,7 +126,7 @@ public void merge_oneSetOfSplits_producesSingleShard() throws Exception { .merge(ImmutableList.of(ImmutableList.of(masterSplit, x86Split)), NO_MAIN_DEX_LIST); ModuleSplit shard = getOnlyElement(shards); - assertThat(shard.getApkTargeting()).isEqualTo(apkAbiTargeting(AbiAlias.X86)); + assertThat(shard.getApkTargeting()).isEqualTo(apkAbiTargeting(X86)); assertSingleEntryStandaloneShard(shard, "lib/x86/libtest.so"); } @@ -136,14 +138,14 @@ public void merge_twoSetsOfSplits_producesTwoShards() throws Exception { .setEntries( ImmutableList.of(InMemoryModuleEntry.ofFile("lib/x86/libtest.so", DUMMY_CONTENT))) .setMasterSplit(false) - .setApkTargeting(apkAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkAbiTargeting(X86)) .build(); ModuleSplit mipsSplit = createModuleSplitBuilder() .setEntries( ImmutableList.of(InMemoryModuleEntry.ofFile("lib/mips/libtest.so", DUMMY_CONTENT))) .setMasterSplit(false) - .setApkTargeting(apkAbiTargeting(AbiAlias.MIPS)) + .setApkTargeting(apkAbiTargeting(MIPS)) .build(); ImmutableList shards = @@ -158,9 +160,9 @@ public void merge_twoSetsOfSplits_producesTwoShards() throws Exception { ImmutableMap shardsByTargeting = Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); assertSingleEntryStandaloneShard( - shardsByTargeting.get(apkAbiTargeting(AbiAlias.X86)), "lib/x86/libtest.so"); + shardsByTargeting.get(apkAbiTargeting(X86)), "lib/x86/libtest.so"); assertSingleEntryStandaloneShard( - shardsByTargeting.get(apkAbiTargeting(AbiAlias.MIPS)), "lib/mips/libtest.so"); + shardsByTargeting.get(apkAbiTargeting(MIPS)), "lib/mips/libtest.so"); } @Test @@ -171,7 +173,7 @@ public void mergeSystemShard_oneSetsOfSplits_producesOneShard() throws Exception .setEntries( ImmutableList.of(InMemoryModuleEntry.ofFile("lib/x86/libtest.so", DUMMY_CONTENT))) .setMasterSplit(false) - .setApkTargeting(apkAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkAbiTargeting(X86)) .build(); ModuleSplit esSplit = createModuleSplitBuilder() @@ -203,7 +205,7 @@ public void mergeSystemShard_oneSetsOfSplits_producesOneShard() throws Exception assertThat(extractPaths(fusedSplit.getEntries())) .containsExactly("lib/x86/libtest.so", "assets/i18n#lang_es/strings.pak"); assertThat(fusedSplit.getApkTargeting()) - .isEqualTo(mergeApkTargeting(apkAbiTargeting(AbiAlias.X86), apkLanguageTargeting("es"))); + .isEqualTo(mergeApkTargeting(apkAbiTargeting(X86), apkLanguageTargeting("es"))); assertThat(fusedSplit.getVariantTargeting()).isEqualToDefaultInstance(); assertThat(fusedSplit.getSplitType()).isEqualTo(SplitType.STANDALONE); assertThat(fusedSplit.isBaseModuleSplit()).isTrue(); @@ -286,21 +288,18 @@ public void mergeSystemShard_multipleModules_producesOneShard() throws Exception assertThat(extractPaths(fusedShard.getEntries())) .containsExactly("assets/i18n#lang_fr/strings.pak", "assets/vr/i18n#lang_fr/strings.pak"); - // es-base, es-vr, it-vr splits. + // es-base, it-base splits. ImmutableList langSplits = shards.getAdditionalSplits(); - assertThat(langSplits).hasSize(3); + assertThat(langSplits).hasSize(2); ImmutableMap langSplitsNameMap = Maps.uniqueIndex(langSplits, split -> split.getAndroidManifest().getSplitId().orElse("")); - assertThat(langSplitsNameMap.keySet()) - .containsExactly("config.es", "vr.config.es", "vr.config.it"); + assertThat(langSplitsNameMap.keySet()).containsExactly("config.es", "config.it"); assertThat(extractPaths(langSplitsNameMap.get("config.es").getEntries())) - .containsExactly("assets/i18n#lang_es/strings.pak"); - assertThat(extractPaths(langSplitsNameMap.get("vr.config.es").getEntries())) - .containsExactly("assets/vr/i18n#lang_es/strings.pak"); - assertThat(extractPaths(langSplitsNameMap.get("vr.config.it").getEntries())) + .containsExactly("assets/i18n#lang_es/strings.pak", "assets/vr/i18n#lang_es/strings.pak"); + assertThat(extractPaths(langSplitsNameMap.get("config.it").getEntries())) .containsExactly("assets/vr/i18n#lang_it/strings.pak"); } @@ -553,7 +552,7 @@ public void splitTargetings_areMerged() throws Exception { // the test. ModuleSplit split1 = createModuleSplitBuilder() - .setApkTargeting(apkAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkAbiTargeting(X86)) .setMasterSplit(false) .build(); ModuleSplit split2 = @@ -569,7 +568,7 @@ public void splitTargetings_areMerged() throws Exception { assertThat(merged.getApkTargeting()) .isEqualTo( mergeApkTargeting( - apkAbiTargeting(AbiAlias.X86), apkDensityTargeting(DensityAlias.HDPI))); + apkAbiTargeting(X86), apkDensityTargeting(DensityAlias.HDPI))); } @Test @@ -578,7 +577,7 @@ public void systemShards_splitTargetingWithLanguages_areMerged() { // the test. ModuleSplit split1 = createModuleSplitBuilder() - .setApkTargeting(apkAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkAbiTargeting(X86)) .setMasterSplit(false) .build(); ModuleSplit split2 = @@ -601,7 +600,7 @@ public void systemShards_splitTargetingWithLanguages_areMerged() { assertThat(merged.getApkTargeting()) .isEqualTo( mergeApkTargeting( - apkAbiTargeting(AbiAlias.X86), + apkAbiTargeting(X86), apkDensityTargeting(DensityAlias.HDPI), apkLanguageTargeting("en"))); } @@ -728,7 +727,7 @@ public void nativeConfigs_defaultAndNonDefault_ok() throws Exception { createModuleSplitBuilder() .setNativeConfig( nativeLibraries( - targetedNativeDirectory("lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + targetedNativeDirectory("lib/mips", nativeDirectoryTargeting(MIPS)))) .build(); ModuleSplit splitDefault = createModuleSplitBuilder().setNativeConfig(NativeLibraries.getDefaultInstance()).build(); @@ -882,7 +881,7 @@ public void mergeApex_oneSetOfSplits_producesOneShard() throws Exception { .setEntries(ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86.img", DUMMY_CONTENT))) .setMasterSplit(false) .setApexConfig(apexConfig) - .setApkTargeting(apkMultiAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkMultiAbiTargeting(X86)) .build(); ImmutableList shards = @@ -890,7 +889,7 @@ public void mergeApex_oneSetOfSplits_producesOneShard() throws Exception { .mergeApex(ImmutableList.of(ImmutableList.of(masterSplit, x86Split))); ModuleSplit x86Shard = getOnlyElement(shards); - assertThat(x86Shard.getApkTargeting()).isEqualTo(apkMultiAbiTargeting(AbiAlias.X86)); + assertThat(x86Shard.getApkTargeting()).isEqualTo(apkMultiAbiTargeting(X86)); assertSingleEntryStandaloneShard(x86Shard, "apex/x86.img"); } @@ -904,7 +903,7 @@ public void mergeApex_twoSetsOfSplits_producesTwoShards() throws Exception { .setEntries(ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86.img", DUMMY_CONTENT))) .setMasterSplit(false) .setApexConfig(apexConfig) - .setApkTargeting(apkMultiAbiTargeting(AbiAlias.X86)) + .setApkTargeting(apkMultiAbiTargeting(X86)) .build(); ModuleSplit mipsSplit = createModuleSplitBuilder() @@ -912,7 +911,7 @@ public void mergeApex_twoSetsOfSplits_producesTwoShards() throws Exception { ImmutableList.of(InMemoryModuleEntry.ofFile("apex/mips.img", DUMMY_CONTENT))) .setMasterSplit(false) .setApexConfig(apexConfig) - .setApkTargeting(apkMultiAbiTargeting(AbiAlias.MIPS)) + .setApkTargeting(apkMultiAbiTargeting(MIPS)) .build(); ImmutableList shards = @@ -927,9 +926,9 @@ public void mergeApex_twoSetsOfSplits_producesTwoShards() throws Exception { Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); assertSingleEntryStandaloneShard( - shardsByTargeting.get(apkMultiAbiTargeting(AbiAlias.X86)), "apex/x86.img"); + shardsByTargeting.get(apkMultiAbiTargeting(X86)), "apex/x86.img"); assertSingleEntryStandaloneShard( - shardsByTargeting.get(apkMultiAbiTargeting(AbiAlias.MIPS)), "apex/mips.img"); + shardsByTargeting.get(apkMultiAbiTargeting(MIPS)), "apex/mips.img"); } @Test @@ -937,12 +936,12 @@ public void mergeApex_twoSetsOfSplits_multipleAbi_producesTwoShards() throws Exc ApexImages apexConfig = ApexImages.getDefaultInstance(); ApkTargeting singleAbiTargeting = apkMultiAbiTargeting( - ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64)), - ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64, AbiAlias.X86))); + ImmutableSet.of(ImmutableSet.of(X86_64)), + ImmutableSet.of(ImmutableSet.of(X86, X86_64))); ApkTargeting doubleAbiTargeting = apkMultiAbiTargeting( - ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64, AbiAlias.X86)), - ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64))); + ImmutableSet.of(ImmutableSet.of(X86, X86_64)), + ImmutableSet.of(ImmutableSet.of(X86_64))); ModuleSplit masterSplit = createModuleSplitBuilder().setMasterSplit(true).setApexConfig(apexConfig).build(); ModuleSplit singleAbiSplit = diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index 5812bff7..8c4c982a 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -61,7 +61,6 @@ import com.android.tools.build.bundletool.TestData; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestFusingException.FusingMissingIncludeAttribute; -import com.android.tools.build.bundletool.model.exceptions.manifest.ManifestVersionException.VersionCodeMissingException; import com.android.tools.build.bundletool.model.utils.xmlproto.UnexpectedAttributeTypeException; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.model.version.Version; @@ -335,14 +334,14 @@ public void getVersionCode() { xmlDecimalIntegerAttribute( ANDROID_NAMESPACE_URI, "versionCode", VERSION_CODE_RESOURCE_ID, 123), xmlNode(xmlElement("application"))))); - assertThat(androidManifest.getVersionCode()).isEqualTo(123); + assertThat(androidManifest.getVersionCode()).hasValue(123); } @Test - public void getVersionCode_missing_throws() { + public void getVersionCode_missing_isEmpty() { AndroidManifest androidManifest = AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(xmlElement("application"))))); - assertThrows(VersionCodeMissingException.class, () -> androidManifest.getVersionCode()); + assertThat(androidManifest.getVersionCode()).isEmpty(); } @Test @@ -869,7 +868,7 @@ public void configSplitPropertiesSet() { Optional.of(false)); assertThat(configManifest.getPackageName()).isEqualTo("com.package.test"); - assertThat(configManifest.getVersionCode()).isEqualTo(1); + assertThat(configManifest.getVersionCode()).hasValue(1); assertThat(configManifest.getHasCode()).hasValue(false); assertThat(configManifest.getSplitId()).hasValue("x86"); assertThat(configManifest.getConfigForSplit()).hasValue("feature1"); @@ -898,7 +897,9 @@ public void configSplit_noExtraElementsFromModuleSplit() throws Exception { Optional.empty()); XmlProtoNode generatedManifest = configManifest.getManifestRoot(); - assertThat(generatedManifest.getProto()).isEqualTo(expectedXmlNodeBuilder.build()); + assertThat(generatedManifest.getProto()) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedXmlNodeBuilder.build()); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/FileSystemModuleEntryTest.java b/src/test/java/com/android/tools/build/bundletool/model/FileSystemModuleEntryTest.java index b76d7752..6fb814b6 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/FileSystemModuleEntryTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/FileSystemModuleEntryTest.java @@ -17,7 +17,6 @@ package com.android.tools.build.bundletool.model; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.tools.build.bundletool.testing.TestUtils; diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java index e5dffbf3..7bc1365d 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -344,7 +344,7 @@ public void apexModuleMultiAbiSplitSuffixAndName() { .build(); resSplit = resSplit.writeSplitIdInManifest(resSplit.getSuffix()); assertThat(resSplit.getAndroidManifest().getSplitId()) - .hasValue("config.x86_64.x86_arm64_v8a.armeabi_v7a"); + .hasValue("config.armeabi_v7a.arm64_v8a_x86.x86_64"); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/ZipPathTest.java b/src/test/java/com/android/tools/build/bundletool/model/ZipPathTest.java index 5572ab7d..ba14e88e 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/ZipPathTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ZipPathTest.java @@ -19,8 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableList; -import java.nio.file.Path; -import java.util.Iterator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -114,7 +112,7 @@ public void testResolve_fromPath() { public void testResolveNull_throws() { ZipPath path = ZipPath.create("foo"); assertThrows(NullPointerException.class, () -> path.resolve((String) null)); - assertThrows(NullPointerException.class, () -> path.resolve((Path) null)); + assertThrows(NullPointerException.class, () -> path.resolve((ZipPath) null)); } @Test @@ -136,7 +134,7 @@ public void testResolveSibling() { @Test public void testResolveSiblingNull_throws() { ZipPath path = ZipPath.create("foo/bar"); - assertThrows(NullPointerException.class, () -> path.resolveSibling((Path) null)); + assertThrows(NullPointerException.class, () -> path.resolveSibling((ZipPath) null)); assertThrows(NullPointerException.class, () -> path.resolveSibling((String) null)); } @@ -286,16 +284,6 @@ public void testToString() { assertThat(ZipPath.create("/foo//bar/").toString()).isEqualTo("foo/bar"); } - @Test - public void testIterator() { - Iterator iterator = ZipPath.create("foo/bar").iterator(); - assertThat(iterator.hasNext()).isTrue(); - assertThat((Object) iterator.next()).isEqualTo(ZipPath.create("foo")); - assertThat(iterator.hasNext()).isTrue(); - assertThat((Object) iterator.next()).isEqualTo(ZipPath.create("bar")); - assertThat(iterator.hasNext()).isFalse(); - } - @Test public void testComparator() { ImmutableList paths = diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java index 0a6945f5..b3d525bc 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/AlternativeVariantTargetingPopulatorTest.java @@ -31,7 +31,6 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; -import com.android.bundle.Targeting.ScreenDensity; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.VariantTargeting; @@ -327,8 +326,7 @@ public void screenDensity_oneVariantDensityAgnostic_throws() throws Exception { public void screenDensity_alternativesPopulated() throws Exception { VariantTargeting ldpiTargeting = variantDensityTargeting(DensityAlias.LDPI); VariantTargeting mdpiTargeting = variantDensityTargeting(DensityAlias.MDPI); - VariantTargeting defaultDensityTargeting = - variantDensityTargeting(ScreenDensity.getDefaultInstance()); + VariantTargeting hdpiTargeting = variantDensityTargeting(DensityAlias.HDPI); ImmutableList outputVariants = new ScreenDensityAlternativesPopulator() @@ -336,7 +334,7 @@ public void screenDensity_alternativesPopulated() throws Exception { ImmutableList.of( createModuleSplit(ldpiTargeting), createModuleSplit(mdpiTargeting), - createModuleSplit(defaultDensityTargeting))); + createModuleSplit(hdpiTargeting))); assertThat(outputVariants).hasSize(3); ModuleSplit ldpiVariantNew = outputVariants.get(0); @@ -346,7 +344,7 @@ public void screenDensity_alternativesPopulated() throws Exception { variantDensityTargeting( toScreenDensity(DensityAlias.LDPI), ImmutableSet.of( - ScreenDensity.getDefaultInstance(), toScreenDensity(DensityAlias.MDPI)))); + toScreenDensity(DensityAlias.MDPI), toScreenDensity(DensityAlias.HDPI)))); ModuleSplit mdpiVariantNew = outputVariants.get(1); assertThat(mdpiVariantNew.getVariantTargeting()) .ignoringRepeatedFieldOrder() @@ -354,13 +352,13 @@ public void screenDensity_alternativesPopulated() throws Exception { variantDensityTargeting( toScreenDensity(DensityAlias.MDPI), ImmutableSet.of( - ScreenDensity.getDefaultInstance(), toScreenDensity(DensityAlias.LDPI)))); - ModuleSplit defaultVariantNew = outputVariants.get(2); - assertThat(defaultVariantNew.getVariantTargeting()) + toScreenDensity(DensityAlias.LDPI), toScreenDensity(DensityAlias.HDPI)))); + ModuleSplit hdpiVariantNew = outputVariants.get(2); + assertThat(hdpiVariantNew.getVariantTargeting()) .ignoringRepeatedFieldOrder() .isEqualTo( variantDensityTargeting( - ScreenDensity.getDefaultInstance(), + toScreenDensity(DensityAlias.HDPI), ImmutableSet.of( toScreenDensity(DensityAlias.LDPI), toScreenDensity(DensityAlias.MDPI)))); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java index b1b3827d..6c2b1d9f 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java @@ -28,7 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; -import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +38,7 @@ public class TargetedDirectorySegmentTest { @Test public void testTargeting_nokey_value_ok() { - TargetedDirectorySegment segment = TargetedDirectorySegment.parse(ZipPath.create("test")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).isEmpty(); assertThat(segment.getTargeting()).isEqualToDefaultInstance(); @@ -47,8 +46,7 @@ public void testTargeting_nokey_value_ok() { @Test public void testTargeting_openGl_ok() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#opengl_2.3")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#opengl_2.3"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.GRAPHICS_API); assertThat(segment.getTargeting()) @@ -58,17 +56,14 @@ public void testTargeting_openGl_ok() { @Test public void testTargeting_openGl_bad_version() { assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#opengl_2.3.3"))); + ValidationException.class, () -> TargetedDirectorySegment.parse("test#opengl_2.3.3")); assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#opengl_x.y"))); + ValidationException.class, () -> TargetedDirectorySegment.parse("test#opengl_x.y")); } @Test public void testTargeting_vulkan_ok() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#vulkan_2.3")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#vulkan_2.3"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.GRAPHICS_API); assertThat(segment.getTargeting()) @@ -78,17 +73,14 @@ public void testTargeting_vulkan_ok() { @Test public void testTargeting_vulkan_bad_version() { assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#vulkan_2.3.3"))); + ValidationException.class, () -> TargetedDirectorySegment.parse("test#vulkan_2.3.3")); assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#vulkan_x.y"))); + ValidationException.class, () -> TargetedDirectorySegment.parse("test#vulkan_x.y")); } @Test public void testTargeting_tcf_astc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_astc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_astc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -100,8 +92,7 @@ public void testTargeting_tcf_astc() { @Test public void testTargeting_tcf_atc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_atc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_atc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -113,8 +104,7 @@ public void testTargeting_tcf_atc() { @Test public void testTargeting_tcf_dxt1() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_dxt1")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_dxt1"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -126,8 +116,7 @@ public void testTargeting_tcf_dxt1() { @Test public void testTargeting_tcf_latc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_latc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_latc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -139,8 +128,7 @@ public void testTargeting_tcf_latc() { @Test public void testTargeting_tcf_paletted() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_paletted")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_paletted"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -152,8 +140,7 @@ public void testTargeting_tcf_paletted() { @Test public void testTargeting_tcf_pvrtc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_pvrtc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_pvrtc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -165,8 +152,7 @@ public void testTargeting_tcf_pvrtc() { @Test public void testTargeting_tcf_etc1() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_etc1")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_etc1"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -178,8 +164,7 @@ public void testTargeting_tcf_etc1() { @Test public void testTargeting_tcf_etc2() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_etc2")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_etc2"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -191,8 +176,7 @@ public void testTargeting_tcf_etc2() { @Test public void testTargeting_tcf_s3tc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_s3tc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_s3tc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -204,8 +188,7 @@ public void testTargeting_tcf_s3tc() { @Test public void testTargeting_tcf_3dc() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#tcf_3dc")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_3dc"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()) .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); @@ -217,41 +200,30 @@ public void testTargeting_tcf_3dc() { @Test public void testTargeting_tcf_badValue() { - assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#tcf_else"))); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("test#tcf_else")); } @Test public void testTargeting_badKey() { assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("test#unsupported_else"))); + ValidationException.class, () -> TargetedDirectorySegment.parse("test#unsupported_else")); } @Test public void testFailsParsing_missingKey() { - assertThrows( - ValidationException.class, () -> TargetedDirectorySegment.parse(ZipPath.create("bad#"))); - assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("bad#_2.0"))); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("bad#")); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("bad#_2.0")); } @Test public void testFailsParsing_missingValue() { - assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("bad#opengl"))); - assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("bad###opengl#"))); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("bad#opengl")); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("bad###opengl#")); } @Test public void testTargeting_language_ok() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#lang_en")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_en"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()).isEqualTo(assetsDirectoryTargeting(languageTargeting("en"))); @@ -259,8 +231,7 @@ public void testTargeting_language_ok() { @Test public void testTargeting_languageThreeChars_ok() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#lang_fil")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_fil"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()) @@ -269,8 +240,7 @@ public void testTargeting_languageThreeChars_ok() { @Test public void testTargeting_upperCase_OK() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#lang_FR")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_FR"); assertThat(segment.getName()).isEqualTo("test"); assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()).isEqualTo(assetsDirectoryTargeting(languageTargeting("fr"))); @@ -278,60 +248,55 @@ public void testTargeting_upperCase_OK() { @Test public void testTargeting_languageFourChars_throws() { - assertThrows( - ValidationException.class, - () -> TargetedDirectorySegment.parse(ZipPath.create("bad#lang_filo"))); + assertThrows(ValidationException.class, () -> TargetedDirectorySegment.parse("bad#lang_filo")); } @Test public void testTargeting_nokey_toPathIdempotent() { - TargetedDirectorySegment segment = TargetedDirectorySegment.parse(ZipPath.create("test")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test"); assertThat(segment.toPathSegment()).isEqualTo("test"); } @Test public void testTargeting_openGl_toPathIdempotent() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#opengl_2.3")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#opengl_2.3"); assertThat(segment.toPathSegment()).isEqualTo("test#opengl_2.3"); } @Test public void testTargeting_vulkan_toPathIdempotent() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#vulkan_2.3")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#vulkan_2.3"); assertThat(segment.toPathSegment()).isEqualTo("test#vulkan_2.3"); } @Test public void testTargeting_tcf_toPathIdempotent() { TargetedDirectorySegment segment; - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_astc")); + segment = TargetedDirectorySegment.parse("test#tcf_astc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_astc"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_atc")); + segment = TargetedDirectorySegment.parse("test#tcf_atc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_atc"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_dxt1")); + segment = TargetedDirectorySegment.parse("test#tcf_dxt1"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_dxt1"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_etc1")); + segment = TargetedDirectorySegment.parse("test#tcf_etc1"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_etc1"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_etc2")); + segment = TargetedDirectorySegment.parse("test#tcf_etc2"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_etc2"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_latc")); + segment = TargetedDirectorySegment.parse("test#tcf_latc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_latc"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_paletted")); + segment = TargetedDirectorySegment.parse("test#tcf_paletted"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_paletted"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_pvrtc")); + segment = TargetedDirectorySegment.parse("test#tcf_pvrtc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_pvrtc"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_s3tc")); + segment = TargetedDirectorySegment.parse("test#tcf_s3tc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_s3tc"); - segment = TargetedDirectorySegment.parse(ZipPath.create("test#tcf_3dc")); + segment = TargetedDirectorySegment.parse("test#tcf_3dc"); assertThat(segment.toPathSegment()).isEqualTo("test#tcf_3dc"); } @Test public void testTargeting_language_toPathIdempotent() { - TargetedDirectorySegment segment = - TargetedDirectorySegment.parse(ZipPath.create("test#lang_fr")); + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_fr"); assertThat(segment.toPathSegment()).isEqualTo("test#lang_fr"); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/PathMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/PathMatcherTest.java new file mode 100755 index 00000000..9ba97308 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/PathMatcherTest.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2019 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 static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.utils.PathMatcher.GlobPatternSyntaxException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class PathMatcherTest { + + @Test + public void testSingleStar() { + PathMatcher globMatcher = PathMatcher.createFromGlob("*.png"); + + assertThat(globMatcher.matches("file.png")).isTrue(); + assertThat(globMatcher.matches("dir/file.png")).isFalse(); + assertThat(globMatcher.matches(".png")).isTrue(); + } + + @Test + public void testSingleStar_withinDirectory() { + PathMatcher globMatcher = PathMatcher.createFromGlob("*dir/*.png"); + + assertThat(globMatcher.matches("file.png")).isFalse(); + assertThat(globMatcher.matches("dir/file.png")).isTrue(); + assertThat(globMatcher.matches("otherdir/file.png")).isTrue(); + assertThat(globMatcher.matches("other/dir/file.png")).isFalse(); + assertThat(globMatcher.matches(".png")).isFalse(); + } + + @Test + public void testDoubleStar() { + PathMatcher globMatcher = PathMatcher.createFromGlob("**.png"); + + assertThat(globMatcher.matches("file.png")).isTrue(); + assertThat(globMatcher.matches("dir/file.png")).isTrue(); + assertThat(globMatcher.matches(".png")).isTrue(); + + assertThat(globMatcher.matches("filepng")).isFalse(); + assertThat(globMatcher.matches("file")).isFalse(); + } + + @Test + public void testDoubleStar_withinDirectory() { + PathMatcher globMatcher = PathMatcher.createFromGlob("dir**/*.png"); + + assertThat(globMatcher.matches("dir/file.png")).isTrue(); + assertThat(globMatcher.matches("dir2/file.png")).isTrue(); + assertThat(globMatcher.matches("dir/dir2/file.png")).isTrue(); + + assertThat(globMatcher.matches("dir.png")).isFalse(); + } + + @Test + public void testQuestionMark() { + PathMatcher globMatcher = PathMatcher.createFromGlob("???"); + + assertThat(globMatcher.matches("abc")).isTrue(); + assertThat(globMatcher.matches("...")).isTrue(); + assertThat(globMatcher.matches("???")).isTrue(); + + assertThat(globMatcher.matches("abcd")).isFalse(); + assertThat(globMatcher.matches("ab")).isFalse(); + } + + @Test + public void testCharacterSet() { + PathMatcher globMatcher = PathMatcher.createFromGlob("[ab]"); + + assertThat(globMatcher.matches("a")).isTrue(); + assertThat(globMatcher.matches("b")).isTrue(); + + assertThat(globMatcher.matches("c")).isFalse(); + assertThat(globMatcher.matches("[")).isFalse(); + assertThat(globMatcher.matches("ab")).isFalse(); + assertThat(globMatcher.matches("abc")).isFalse(); + assertThat(globMatcher.matches("[ab]")).isFalse(); + } + + @Test + public void testCharacterSet_numberRange() { + PathMatcher globMatcher = PathMatcher.createFromGlob("[0-9]"); + + assertThat(globMatcher.matches("0")).isTrue(); + assertThat(globMatcher.matches("5")).isTrue(); + assertThat(globMatcher.matches("9")).isTrue(); + + assertThat(globMatcher.matches("00")).isFalse(); + assertThat(globMatcher.matches("99")).isFalse(); + assertThat(globMatcher.matches("0-9")).isFalse(); + assertThat(globMatcher.matches("[0-9]")).isFalse(); + } + + @Test + public void testCharacterSet_letterRange() { + PathMatcher globMatcher = PathMatcher.createFromGlob("[A-Z]"); + + assertThat(globMatcher.matches("A")).isTrue(); + assertThat(globMatcher.matches("R")).isTrue(); + assertThat(globMatcher.matches("Z")).isTrue(); + + assertThat(globMatcher.matches("a")).isFalse(); + assertThat(globMatcher.matches("AA")).isFalse(); + assertThat(globMatcher.matches("ZZ")).isFalse(); + assertThat(globMatcher.matches("A-Z")).isFalse(); + assertThat(globMatcher.matches("[A-Z]")).isFalse(); + } + + @Test + public void testCharacterSet_multipleRanges() { + PathMatcher globMatcher = PathMatcher.createFromGlob("[A-Za-z]"); + + assertThat(globMatcher.matches("A")).isTrue(); + assertThat(globMatcher.matches("B")).isTrue(); + assertThat(globMatcher.matches("Z")).isTrue(); + assertThat(globMatcher.matches("a")).isTrue(); + assertThat(globMatcher.matches("b")).isTrue(); + assertThat(globMatcher.matches("z")).isTrue(); + + assertThat(globMatcher.matches("0")).isFalse(); + assertThat(globMatcher.matches("AA")).isFalse(); + assertThat(globMatcher.matches("Za")).isFalse(); + } + + @Test + public void testCharacterSet_caretEscaped() { + PathMatcher globMatcher = PathMatcher.createFromGlob("[^a]"); + assertThat(globMatcher.matches("a")).isTrue(); + assertThat(globMatcher.matches("^")).isTrue(); + + assertThat(globMatcher.matches("b")).isFalse(); + } + + @Test + public void testCharacterSet_withSlash_throws() { + Exception expected = + assertThrows(GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("[ab/]")); + assertThat(expected) + .hasMessageThat() + .contains("Character '/' is not allowed within a character set"); + assertThat(expected).hasMessageThat().contains("at character 4"); + } + + @Test + public void testCharacterSet_emptySet_throws() { + Exception expected = + assertThrows(GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("abc[]")); + assertThat(expected).hasMessageThat().contains("Empty characters set"); + assertThat(expected).hasMessageThat().contains("at character 4"); + } + + @Test + public void testCharacterSet_notClosed_throws() { + Exception expected = + assertThrows(GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("a[bc")); + assertThat(expected).hasMessageThat().contains("No matching ']' found"); + assertThat(expected).hasMessageThat().contains("at character 2"); + } + + @Test + public void testCharacterSet_onlyClosed_throws() { + Exception expected = + assertThrows(GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("ab]c")); + assertThat(expected).hasMessageThat().contains("No matching '[' found"); + assertThat(expected).hasMessageThat().contains("at character 3"); + } + + @Test + public void testGroup() { + PathMatcher globMatcher = PathMatcher.createFromGlob("{temp*,tmp*}"); + + assertThat(globMatcher.matches("temp")).isTrue(); + assertThat(globMatcher.matches("temporary")).isTrue(); + assertThat(globMatcher.matches("tmp")).isTrue(); + assertThat(globMatcher.matches("tmpfile")).isTrue(); + + assertThat(globMatcher.matches("{tmp}")).isFalse(); + assertThat(globMatcher.matches("tmp/file")).isFalse(); + } + + @Test + public void testGroup_emptyPart() { + PathMatcher globMatcher = PathMatcher.createFromGlob("file{,.png}"); + + assertThat(globMatcher.matches("file")).isTrue(); + assertThat(globMatcher.matches("file.png")).isTrue(); + + assertThat(globMatcher.matches("fileapng")).isFalse(); + assertThat(globMatcher.matches("file,")).isFalse(); + } + + @Test + public void testGroup_characterSetsWithinGroup() { + PathMatcher globMatcher = PathMatcher.createFromGlob("file.{[0-9],[a-z][0-9]}"); + + assertThat(globMatcher.matches("file.0")).isTrue(); + assertThat(globMatcher.matches("file.1")).isTrue(); + assertThat(globMatcher.matches("file.b2")).isTrue(); + + assertThat(globMatcher.matches("file.a")).isFalse(); + assertThat(globMatcher.matches("file.11")).isFalse(); + } + + @Test + public void testNestedGroup_throws() { + Exception expected = + assertThrows( + GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("{tmp{1,2},temp}")); + assertThat(expected).hasMessageThat().contains("Cannot nest groups"); + assertThat(expected).hasMessageThat().contains("at character 5"); + } + + @Test + public void testGroupNotClosed_throws() { + Exception expected = + assertThrows( + GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("a{tmp,temp")); + assertThat(expected).hasMessageThat().contains("No matching '}' found"); + assertThat(expected).hasMessageThat().contains("at character 2"); + } + + @Test + public void testGroupOnlyClosed_throws() { + Exception expected = + assertThrows(GlobPatternSyntaxException.class, () -> PathMatcher.createFromGlob("ab}c")); + assertThat(expected).hasMessageThat().contains("No matching '{' found"); + assertThat(expected).hasMessageThat().contains("at character 3"); + } + + @Test + public void testMultipleGroups() { + PathMatcher pathMatcher = PathMatcher.createFromGlob("{a,b}{c,d}"); + assertThat(pathMatcher.matches("ac")).isTrue(); + assertThat(pathMatcher.matches("ad")).isTrue(); + assertThat(pathMatcher.matches("bc")).isTrue(); + assertThat(pathMatcher.matches("bd")).isTrue(); + + assertThat(pathMatcher.matches("aa")).isFalse(); + } + + @Test + public void testSpecialRegexpCharactersAreEscaped() { + PathMatcher globMatcher = PathMatcher.createFromGlob("<(^-=$!|)+.>"); + + assertThat(globMatcher.matches("<(^-=$!|)+.>")).isTrue(); + } + + @Test + public void testEscapeSpecialCharacters() { + PathMatcher globMatcher = PathMatcher.createFromGlob("\\*.png"); + assertThat(globMatcher.matches("*.png")).isTrue(); + assertThat(globMatcher.matches("file.png")).isFalse(); + + globMatcher = PathMatcher.createFromGlob("\\?.png"); + assertThat(globMatcher.matches("?.png")).isTrue(); + assertThat(globMatcher.matches("f.png")).isFalse(); + + globMatcher = PathMatcher.createFromGlob("\\\\?png"); + assertThat(globMatcher.matches("\\.png")).isTrue(); + assertThat(globMatcher.matches("\\png")).isFalse(); + } + + @Test + public void testNegation() { + PathMatcher globMatcher = PathMatcher.createFromGlob("a[!bc]*"); + + assertThat(globMatcher.matches("ad")).isTrue(); + assertThat(globMatcher.matches("adb")).isTrue(); + + assertThat(globMatcher.matches("ab")).isFalse(); + assertThat(globMatcher.matches("abc")).isFalse(); + assertThat(globMatcher.matches("ac")).isFalse(); + } + + @Test + public void testUnicodeCharacters() { + PathMatcher globMatcher = PathMatcher.createFromGlob("emoji.\uD83D\uDE00"); + + assertThat(globMatcher.matches("emoji.\uD83D\uDE00")).isTrue(); + } + + @Test + public void testCommaWithinCharacterSetWithinGroup() { + PathMatcher pathMatcher = PathMatcher.createFromGlob("{ab,[ab,]}"); + + assertThat(pathMatcher.matches(",")).isTrue(); + assertThat(pathMatcher.matches("ab")).isTrue(); + assertThat(pathMatcher.matches("a")).isTrue(); + + assertThat(pathMatcher.matches("ab,")).isFalse(); + assertThat(pathMatcher.matches("a,")).isFalse(); + } + + @Test + public void testComplex() { + PathMatcher pathMatcher = PathMatcher.createFromGlob("a{b[cd]*/e,[fg1-9]/h,k**.p}i"); + + assertThat(pathMatcher.matches("abc/ei")).isTrue(); + assertThat(pathMatcher.matches("abdxx/ei")).isTrue(); + assertThat(pathMatcher.matches("af/hi")).isTrue(); + assertThat(pathMatcher.matches("a5/hi")).isTrue(); + assertThat(pathMatcher.matches("ak.pi")).isTrue(); + assertThat(pathMatcher.matches("ak/l/.pi")).isTrue(); + + assertThat(pathMatcher.matches("abc/x/ei")).isFalse(); + assertThat(pathMatcher.matches("a/ei")).isFalse(); + assertThat(pathMatcher.matches("a/0hi")).isFalse(); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java index 0592c9fd..a1967b1a 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java @@ -27,12 +27,14 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSystemApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.Variant; import com.android.bundle.Targeting.Abi.AbiAlias; @@ -211,10 +213,49 @@ public void isSystemApkVariantTrue() throws Exception { assertThat(ResultUtils.isSystemApkVariant(variant)).isTrue(); } + @Test + public void getAllTargetedLanguages() { + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("master.apk")), + createApkDescription( + apkLanguageTargeting("en"), + ZipPath.create("en.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkLanguageTargeting("pl"), + ZipPath.create("pl.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkLanguageTargeting("ru"), + ZipPath.create("ru.apk"), + /* isMasterSplit= */ false)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .addApkDescription( + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("assets.apk"))) + .addApkDescription( + createApkDescription( + apkLanguageTargeting("fr"), + ZipPath.create("fr.apk"), + /* isMasterSplit= */ false)) + .build()) + .build(); + ImmutableSet langs = ResultUtils.getAllTargetedLanguages(tableOfContentsProto); + assertThat(langs).containsExactly("pl", "en", "ru", "fr"); + } + private Variant createInstantVariant() { - Path apkLBase = ZipPath.create("instant/apkL-base.apk"); - Path apkLFeature = ZipPath.create("instant/apkL-feature.apk"); - Path apkLOther = ZipPath.create("instant/apkL-other.apk"); + ZipPath apkLBase = ZipPath.create("instant/apkL-base.apk"); + ZipPath apkLFeature = ZipPath.create("instant/apkL-feature.apk"); + ZipPath apkLOther = ZipPath.create("instant/apkL-other.apk"); return createVariant( variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), createInstantApkSet("base", ApkTargeting.getDefaultInstance(), apkLBase), @@ -237,14 +278,14 @@ private Variant createSplitVariant() { } private Variant createStandaloneVariant() { - Path apkPreL = ZipPath.create("apkPreL.apk"); + ZipPath apkPreL = ZipPath.create("apkPreL.apk"); return createVariant( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(SdkVersion.getDefaultInstance())), createStandaloneApkSet(ApkTargeting.getDefaultInstance(), apkPreL)); } private Variant createSystemVariant() { - Path systemApk = ZipPath.create("system.apk"); + ZipPath systemApk = ZipPath.create("system.apk"); return createVariant( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(SdkVersion.getDefaultInstance())), createSystemApkSet(ApkTargeting.getDefaultInstance(), systemApk)); diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java new file mode 100755 index 00000000..5c712bb5 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model.utils; + +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.HDPI; +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.LDPI; +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.MDPI; +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.NODPI; +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.XHDPI; +import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.XXXHDPI; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.toScreenDensity; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.ScreenDensityTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.testing.ProtoFuzzer; +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 TargetingNormalizerTest { + @Test + public void normalizeScreenDensityTargeting() { + ApkTargeting targeting = + apkDensityTargeting( + ScreenDensityTargeting.newBuilder() + .addAllValue( + ImmutableList.of( + toScreenDensity(LDPI), + toScreenDensity(MDPI), + toScreenDensity(HDPI), + toScreenDensity(150), + toScreenDensity(200))) + .addAllAlternatives( + ImmutableList.of( + toScreenDensity(XHDPI), + toScreenDensity(XXXHDPI), + toScreenDensity(NODPI), + toScreenDensity(400), + toScreenDensity(800))) + .build()); + + ApkTargeting normalized = TargetingNormalizer.normalizeApkTargeting(targeting); + + assertThat(normalized) + .isEqualTo( + apkDensityTargeting( + ScreenDensityTargeting.newBuilder() + .addAllValue( + ImmutableList.of( + toScreenDensity(LDPI), + toScreenDensity(150), + toScreenDensity(MDPI), + toScreenDensity(200), + toScreenDensity(HDPI))) + .addAllAlternatives( + ImmutableList.of( + toScreenDensity(XHDPI), + toScreenDensity(400), + toScreenDensity(XXXHDPI), + toScreenDensity(800), + toScreenDensity(NODPI))) + .build())); + } + + @Test + public void normalizeApkTargeting_allTargetingDimensionsAreHandled() { + ApkTargeting apkTargeting = ProtoFuzzer.randomProtoMessage(ApkTargeting.class); + ApkTargeting shuffledApkTargeting = ProtoFuzzer.shuffleRepeatedFields(apkTargeting); + // Sanity-check that the testing data was generated alright. + assertThat(apkTargeting).isNotEqualTo(shuffledApkTargeting); + + // The following check fails, if the normalizing logic forgets to handle some dimension. + // This would typically happen when the targeting proto is extended by a new dimension. + assertThat(TargetingNormalizer.normalizeApkTargeting(apkTargeting)) + .isEqualTo(TargetingNormalizer.normalizeApkTargeting(shuffledApkTargeting)); + } + + @Test + public void normalizeVariantTargeting_allTargetingDimensionsAreHandled() { + VariantTargeting variantTargeting = ProtoFuzzer.randomProtoMessage(VariantTargeting.class); + VariantTargeting shuffledVariantTargeting = ProtoFuzzer.shuffleRepeatedFields(variantTargeting); + // Sanity-check that the testing data was generated alright. + assertThat(variantTargeting).isNotEqualTo(shuffledVariantTargeting); + + // The following check fails, if the normalizing logic forgets to handle some dimension. + // This would typically happen when the targeting proto is extended by a new dimension. + assertThat(TargetingNormalizer.normalizeVariantTargeting(variantTargeting)) + .isEqualTo(TargetingNormalizer.normalizeVariantTargeting(shuffledVariantTargeting)); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ThrowableUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ThrowableUtilsTest.java index f9fcdb3a..7963756d 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ThrowableUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ThrowableUtilsTest.java @@ -85,8 +85,8 @@ public void anyCauseOrSuppressedMatches_nothingMatches() { @Test public void anyCauseOrSuppressedMatches_handlesCausalLoop() { - MutableThrowable throwable1 = new MutableThrowable(); - MutableThrowable throwable2 = new MutableThrowable(); + MutableException throwable1 = new MutableException(); + MutableException throwable2 = new MutableException(); throwable1.cause = throwable2; throwable2.cause = throwable1; @@ -100,7 +100,7 @@ public void anyCauseOrSuppressedMatches_handlesCausalLoop() { assertThat(exception).hasMessageThat().contains("causal chain detected"); } - static class MutableThrowable extends Throwable { + static class MutableException extends Exception { Throwable cause; @Override diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ZipUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ZipUtilsTest.java index 87b837da..b21a82fb 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ZipUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ZipUtilsTest.java @@ -52,7 +52,7 @@ public void allFileEntries_multipleFiles() throws Exception { try (ZipFile zipFile = createZipFileWithFiles("a", "b", "c")) { ImmutableList files = ZipUtils.allFileEntriesPaths(zipFile).collect(toImmutableList()); - assertThat(files.stream().map(Path::toString).collect(toList())) + assertThat(files.stream().map(ZipPath::toString).collect(toList())) .containsExactly("a", "b", "c"); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/files/FileUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/files/FileUtilsTest.java index d88dc58f..363be97e 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/files/FileUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/files/FileUtilsTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; +import com.android.tools.build.bundletool.model.ZipPath; import java.io.ByteArrayInputStream; import java.nio.file.Path; import java.nio.file.Paths; @@ -113,42 +114,42 @@ public void equalContent_differentLength() throws Exception { @Test public void getExtension_emptyFile() { - assertThat(FileUtils.getFileExtension(Paths.get(""))).isEmpty(); + assertThat(FileUtils.getFileExtension(ZipPath.create(""))).isEmpty(); } @Test public void getExtension_fileNoExtension() { - assertThat(FileUtils.getFileExtension(Paths.get("file"))).isEmpty(); + assertThat(FileUtils.getFileExtension(ZipPath.create("file"))).isEmpty(); } @Test public void getExtension_fileInDirectoryNoExtension() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file"))).isEmpty(); + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file"))).isEmpty(); } @Test public void getExtension_fileInDirectoryNoExtension_EndWithDot() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file."))).isEmpty(); + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file."))).isEmpty(); } @Test public void getExtension_fileInDirectoryOneLetterExtension() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file.a"))).isEqualTo("a"); + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file.a"))).isEqualTo("a"); } @Test public void getExtension_fileInDirectorySimpleExtension() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file.txt"))).isEqualTo("txt"); + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file.txt"))).isEqualTo("txt"); } @Test public void getExtension_fileInDirectorySimpleExtension_EndsWithDot() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file.txt."))).isEqualTo(""); + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file.txt."))).isEmpty(); } @Test public void getExtension_fileInDirectoryDoubleExtension() { - assertThat(FileUtils.getFileExtension(Paths.get("directory", "file.pb.json"))) + assertThat(FileUtils.getFileExtension(ZipPath.create("directory/file.pb.json"))) .isEqualTo("json"); } } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java index bd3d1b3f..00f768d5 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java @@ -125,14 +125,14 @@ public void splittingByMultipleAbi_multipleImageFiles() throws Exception { assertThat(splits).hasSize(6); assertThat(splits.stream().map(ModuleSplit::getVariantTargeting).collect(toImmutableSet())) .containsExactly(lPlusVariantTargeting()); - ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); - ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64X86Set = ImmutableSet.of(X86, X86_64); + ImmutableSet x64ArmSet = ImmutableSet.of(ARMEABI_V7A, X86_64); ImmutableSet x64Set = ImmutableSet.of(X86_64); - ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86ArmSet = ImmutableSet.of(ARMEABI_V7A, X86); ImmutableSet x86Set = ImmutableSet.of(X86); ImmutableSet armSet = ImmutableSet.of(ARMEABI_V7A); ImmutableSet> allTargeting = - ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, armSet); + ImmutableSet.of(armSet, x86ArmSet, x64ArmSet, x86Set, x64X86Set, x64Set); ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); ApkTargeting a64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java index 61d00f12..a56d0de7 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java @@ -34,6 +34,7 @@ import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; import static com.android.tools.build.bundletool.testing.TestUtils.getEntryContent; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import com.android.bundle.Config.SuffixStripping; @@ -58,11 +59,12 @@ @RunWith(JUnit4.class) public class AssetModuleSplitterTest { + private static final String MODULE_NAME = "test_module"; @Test public void singleSlice() throws Exception { BundleModule testModule = - new BundleModuleBuilder("testModule") + new BundleModuleBuilder(MODULE_NAME) .addFile("assets/image.jpg") .addFile("assets/image2.jpg") .setManifest(androidManifestForAssetModule("com.test.app")) @@ -77,6 +79,7 @@ public void singleSlice() throws Exception { ModuleSplit masterSlice = slices.get(0); assertThat(masterSlice.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); assertThat(masterSlice.isMasterSplit()).isTrue(); + assertThat(masterSlice.getAndroidManifest().getSplitId()).hasValue(MODULE_NAME); assertThat(masterSlice.getApkTargeting()).isEqualToDefaultInstance(); assertThat(extractPaths(masterSlice.getEntries())) .containsExactly("assets/image.jpg", "assets/image2.jpg"); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AssetSlicesGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AssetSlicesGeneratorTest.java index c0241977..688c5cf9 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AssetSlicesGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AssetSlicesGeneratorTest.java @@ -29,8 +29,8 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Targeting.ApkTargeting; import com.android.tools.build.bundletool.model.AppBundle; @@ -91,7 +91,12 @@ public void upfrontAssetModule_addsVersionCode() throws Exception { assertThat(assetSlices).hasSize(1); ModuleSplit assetSlice = assetSlices.get(0); - assertThat(assetSlice.getAndroidManifest().getVersionCode()).isEqualTo(VERSION_CODE); + assertThat( + assetSlice + .getAndroidManifest() + .getVersionCode() + .orElseThrow(VersionCodeMissingException::new)) + .isEqualTo(VERSION_CODE); } @Test @@ -108,8 +113,7 @@ public void onDemandAssetModule_leavesVersionCodeEmpty() throws Exception { assertThat(assetSlices).hasSize(1); ModuleSplit assetSlice = assetSlices.get(0); - assertThrows( - VersionCodeMissingException.class, () -> assetSlice.getAndroidManifest().getVersionCode()); + assertThat(assetSlice.getAndroidManifest().getVersionCode()).isEmpty(); } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AssetsLanguageSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AssetsLanguageSplitterTest.java index b0af0ef1..f644fa73 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AssetsLanguageSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AssetsLanguageSplitterTest.java @@ -24,7 +24,6 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assets; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; -import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithDefaultTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithTargetingEqualTo; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; @@ -52,7 +51,7 @@ public class AssetsLanguageSplitterTest { @Test - public void singleSplit() throws Exception { + public void singleAndEmptyDefaultSplit() throws Exception { BundleModule testModule = new BundleModuleBuilder("testModule") .addFile("assets/i18n#lang_jp/strings.pak") @@ -66,8 +65,9 @@ public void singleSplit() throws Exception { ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); Collection assetsSplits = LanguageAssetsSplitter.create().split(baseSplit); - assertThat(assetsSplits).hasSize(1); + assertThat(assetsSplits).hasSize(2); ModuleSplit split = assetsSplits.iterator().next(); + verifySplitFor(assetsSplits, ApkTargeting.getDefaultInstance()); assertThat(split.getApkTargeting()).isEqualTo(apkLanguageTargeting(languageTargeting("jp"))); assertThat(split.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); assertThat(extractPaths(split.getEntries())) @@ -127,7 +127,7 @@ public void languageAlternativesSplit() throws Exception { ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); Collection assetsSplits = LanguageAssetsSplitter.create().split(baseSplit); - assertThat(assetsSplits).hasSize(2); + assertThat(assetsSplits).hasSize(3); assertThat( assetsSplits .stream() @@ -135,7 +135,7 @@ public void languageAlternativesSplit() throws Exception { .distinct() .collect(toImmutableSet())) .containsExactly(lPlusVariantTargeting()); - assertThat(getSplitsWithDefaultTargeting(assetsSplits)).isEmpty(); + verifySplitFor(assetsSplits, ApkTargeting.getDefaultInstance()); verifySplitFor( assetsSplits, apkLanguageTargeting("jp"), @@ -175,7 +175,7 @@ public void languageAlternativesSplit_multipleDistinctGroups() throws Exception ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); Collection assetsSplits = LanguageAssetsSplitter.create().split(baseSplit); - assertThat(assetsSplits).hasSize(4); + assertThat(assetsSplits).hasSize(5); assertThat( assetsSplits .stream() @@ -183,6 +183,7 @@ public void languageAlternativesSplit_multipleDistinctGroups() throws Exception .distinct() .collect(toImmutableSet())) .containsExactly(lPlusVariantTargeting()); + verifySplitFor(assetsSplits, ApkTargeting.getDefaultInstance()); verifySplitFor( assetsSplits, apkLanguageTargeting("jp"), diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java index 9958dd32..8110301c 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java @@ -23,6 +23,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.ACTIVITY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.DEFAULT_DENSITY_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.HDPI_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.LDPI_VALUE; @@ -248,11 +249,15 @@ public void shardByNoDimension_keepMultipleTcfTargetedDirectories() throws Excep targetedAssetsDirectory( "assets/data#tcf_etc1", assetsDirectoryTargeting( - textureCompressionTargeting(TextureCompressionFormatAlias.ETC1_RGB8))), + textureCompressionTargeting( + TextureCompressionFormatAlias.ETC1_RGB8, + ImmutableSet.of(TextureCompressionFormatAlias.ATC)))), targetedAssetsDirectory( "assets/data#tcf_atc", assetsDirectoryTargeting( - textureCompressionTargeting(TextureCompressionFormatAlias.ATC))))) + textureCompressionTargeting( + TextureCompressionFormatAlias.ATC, + ImmutableSet.of(TextureCompressionFormatAlias.ETC1_RGB8)))))) .build(); BundleSharder bundleSharder = @@ -724,21 +729,17 @@ public void shardByAbi_havingManyModulesWithLanguagesForSystemShards() throws Ex "assets/vr/languages#lang_es/image.jpg", "assets/languages#lang_es/image.jpg"); ImmutableList langSplits = shards.getAdditionalSplits(); - assertThat(langSplits).hasSize(2); + assertThat(langSplits).hasSize(1); ImmutableMap langSplitsNameMap = Maps.uniqueIndex(langSplits, split -> split.getModuleName().getName()); - assertThat(langSplitsNameMap.keySet()).containsExactly("base", "vr"); + assertThat(langSplitsNameMap.keySet()).containsExactly("base"); ModuleSplit frBaseSplit = langSplitsNameMap.get("base"); assertThat(extractPaths(frBaseSplit.getEntries())) - .containsExactly("assets/languages#lang_fr/image.jpg"); + .containsExactly( + "assets/languages#lang_fr/image.jpg", "assets/vr/languages#lang_fr/image.jpg"); assertThat(frBaseSplit.getAndroidManifest().getSplitId()).hasValue("config.fr"); - - ModuleSplit frVrSplit = langSplitsNameMap.get("vr"); - assertThat(extractPaths(frVrSplit.getEntries())) - .containsExactly("assets/vr/languages#lang_fr/image.jpg"); - assertThat(frVrSplit.getAndroidManifest().getSplitId()).hasValue("vr.config.fr"); } @Ignore @@ -863,14 +864,14 @@ public void shardApexModule() throws Exception { .distinct() .collect(toImmutableSet())) .containsExactly(VariantTargeting.getDefaultInstance()); - ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); - ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64X86Set = ImmutableSet.of(X86, X86_64); + ImmutableSet x64ArmSet = ImmutableSet.of(ARMEABI_V7A, X86_64); ImmutableSet x64Set = ImmutableSet.of(X86_64); - ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86ArmSet = ImmutableSet.of(ARMEABI_V7A, X86); ImmutableSet x86Set = ImmutableSet.of(X86); ImmutableSet armSet = ImmutableSet.of(ARMEABI_V7A); ImmutableSet> allTargeting = - ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, armSet); + ImmutableSet.of(armSet, x86ArmSet, x64ArmSet, x86Set, x64X86Set, x64Set); ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); ApkTargeting a64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); @@ -1513,7 +1514,7 @@ public void shardByAbiAndDensity_havingManyAbisAndSomeResource_producesManyApks( shard.getApkTargeting().getScreenDensityTargeting().getValue(0).getDensityAlias(); switch (density) { case LDPI: - assertThat(extractPaths(shard.findEntriesUnderPath("res").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res"))) .containsExactly("res/drawable-ldpi/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1523,7 +1524,7 @@ public void shardByAbiAndDensity_havingManyAbisAndSomeResource_producesManyApks( case MDPI: // MDPI is a special case because the bucket encompasses devices that could serve either // the LDPI or the HDPI resource, so both resources are present. - assertThat(extractPaths(shard.findEntriesUnderPath("res").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res"))) .containsExactly("res/drawable-ldpi/image.jpg", "res/drawable-hdpi/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1535,7 +1536,7 @@ public void shardByAbiAndDensity_havingManyAbisAndSomeResource_producesManyApks( case XHDPI: case XXHDPI: case XXXHDPI: - assertThat(extractPaths(shard.findEntriesUnderPath("res").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res"))) .containsExactly("res/drawable-hdpi/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1849,7 +1850,7 @@ public void manyModulesShardByDensity_havingOnlyOneDensityResource_producesSingl assertThat(shards).hasSize(1); ModuleSplit shard = shards.get(0); - assertThat(extractPaths(shard.findEntriesUnderPath("res").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res"))) .containsExactly("res/drawable-hdpi/image.jpg", "res/drawable-hdpi/image2.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1978,7 +1979,7 @@ public void manyModulesShardByAbiAndDensity_havingManyAbisAndSomeResource_produc switch (density) { case LDPI: case MDPI: - assertThat(extractPaths(shard.findEntriesUnderPath("res/").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res/"))) .containsExactly("res/drawable/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1988,7 +1989,7 @@ public void manyModulesShardByAbiAndDensity_havingManyAbisAndSomeResource_produc case TVDPI: // TVDPI is a special case because the bucket encompasses devices that could serve either // the MDPI or the HDPI resource, so both resources are present. - assertThat(extractPaths(shard.findEntriesUnderPath("res/").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res/"))) .containsExactly("res/drawable/image.jpg", "res/drawable-hdpi/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") @@ -1999,7 +2000,7 @@ public void manyModulesShardByAbiAndDensity_havingManyAbisAndSomeResource_produc case XHDPI: case XXHDPI: case XXXHDPI: - assertThat(extractPaths(shard.findEntriesUnderPath("res/").collect(toImmutableList()))) + assertThat(extractPaths(shard.findEntriesUnderPath("res/"))) .containsExactly("res/drawable-hdpi/image.jpg"); assertThat(shard.getResourceTable().get()) .containsResource("com.test.app:drawable/image") diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/GraphicsApiAssetsSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/GraphicsApiAssetsSplitterTest.java index e3e4170f..72d76b09 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/GraphicsApiAssetsSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/GraphicsApiAssetsSplitterTest.java @@ -26,7 +26,6 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithDefaultTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithTargetingEqualTo; import static com.android.tools.build.bundletool.testing.TargetingUtils.graphicsApiTargeting; -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.mergeAssetsTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.openGlVersionFrom; @@ -37,7 +36,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Targeting.Abi.AbiAlias; @@ -62,7 +60,7 @@ public class GraphicsApiAssetsSplitterTest { @Test - public void singleSplit() throws Exception { + public void singleOpenGlSplitAndEmptyDefaultSplit() throws Exception { BundleModule testModule = new BundleModuleBuilder("testModule") .addFile("assets/images#opengl_2.0/image.jpg") @@ -77,13 +75,21 @@ public void singleSplit() throws Exception { ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); Collection assetsSplits = GraphicsApiAssetsSplitter.create().split(baseSplit); - assertThat(assetsSplits).hasSize(1); - ModuleSplit split = assetsSplits.iterator().next(); - assertThat(split.getApkTargeting()) - .isEqualTo(apkGraphicsTargeting(graphicsApiTargeting(openGlVersionFrom(2)))); - assertThat(split.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); - assertThat(split.getSplitType()).isEqualTo(SplitType.SPLIT); - assertThat(extractPaths(split.getEntries())) + assertThat(assetsSplits).hasSize(2); + assertThat( + assetsSplits.stream() + .map(ModuleSplit::getSplitType) + .distinct() + .collect(toImmutableSet())) + .containsExactly(SplitType.SPLIT); + List defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); + assertThat(defaultSplits).hasSize(1); + assertThat(extractPaths(defaultSplits.get(0).getEntries())).isEmpty(); + List gl2PlusSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, apkGraphicsTargeting(graphicsApiTargeting(openGlVersionFrom(2)))); + assertThat(gl2PlusSplits).hasSize(1); + assertThat(extractPaths(gl2PlusSplits.get(0).getEntries())) .containsExactly( "assets/images#opengl_2.0/image.jpg", "assets/images#opengl_2.0/image2.jpg"); } @@ -216,7 +222,7 @@ public void multipleVersionsWithOpenRanges() throws Exception { ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); Collection assetsSplits = GraphicsApiAssetsSplitter.create().split(baseSplit); - assertThat(assetsSplits).hasSize(3); + assertThat(assetsSplits).hasSize(4); assertThat( assetsSplits .stream() @@ -254,6 +260,10 @@ public void multipleVersionsWithOpenRanges() throws Exception { assertThat(gl3Splits).hasSize(1); assertThat(extractPaths(gl3Splits.get(0).getEntries())) .containsExactly("assets/images#opengl_3.0/image.jpg"); + List defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); + assertThat(defaultSplits).hasSize(1); + assertThat(defaultSplits.get(0).isMasterSplit()).isTrue(); + assertThat(extractPaths(defaultSplits.get(0).getEntries())).isEmpty(); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java index 95ef98ae..d81590af 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java @@ -441,12 +441,11 @@ public void multipleModule_withLanguages() throws Exception { mergeSpecs( sdkVersion(28), abis("x86"), density(DensityAlias.MDPI), locales("fr")))); - assertThat(moduleSplits).hasSize(4); // fused, base-es, vr-es, vr-it splits + assertThat(moduleSplits).hasSize(3); // fused, base-es, vr-es, vr-it splits ImmutableMap splitBySplitIdMap = Maps.uniqueIndex(moduleSplits, split -> split.getAndroidManifest().getSplitId().orElse("")); - assertThat(splitBySplitIdMap.keySet()) - .containsExactly("", "config.es", "vr.config.es", "vr.config.it"); + assertThat(splitBySplitIdMap.keySet()).containsExactly("", "config.es", "config.it"); ModuleSplit fusedSplit = splitBySplitIdMap.get(""); assertThat(fusedSplit.getApkTargeting()).isEqualTo(apkLanguageTargeting("fr")); @@ -462,22 +461,15 @@ public void multipleModule_withLanguages() throws Exception { assertThat(esBaseSplit.getSplitType()).isEqualTo(SplitType.SYSTEM); assertThat(esBaseSplit.isMasterSplit()).isFalse(); assertThat(extractPaths(esBaseSplit.getEntries())) - .containsExactly("assets/languages#lang_es/image.jpg"); - - ModuleSplit esVrSplit = splitBySplitIdMap.get("vr.config.es"); - assertThat(esVrSplit.getApkTargeting()).isEqualTo(apkLanguageTargeting("es")); - assertThat(esVrSplit.getVariantTargeting()).isEqualTo(fusedSplit.getVariantTargeting()); - assertThat(esVrSplit.getSplitType()).isEqualTo(SplitType.SYSTEM); - assertThat(esVrSplit.isMasterSplit()).isFalse(); - assertThat(extractPaths(esVrSplit.getEntries())) - .containsExactly("assets/vr/languages#lang_es/image.jpg"); - - ModuleSplit itVrSplit = splitBySplitIdMap.get("vr.config.it"); - assertThat(itVrSplit.getApkTargeting()).isEqualTo(apkLanguageTargeting("it")); - assertThat(itVrSplit.getVariantTargeting()).isEqualTo(fusedSplit.getVariantTargeting()); - assertThat(itVrSplit.getSplitType()).isEqualTo(SplitType.SYSTEM); - assertThat(itVrSplit.isMasterSplit()).isFalse(); - assertThat(extractPaths(itVrSplit.getEntries())) + .containsExactly( + "assets/languages#lang_es/image.jpg", "assets/vr/languages#lang_es/image.jpg"); + + ModuleSplit itBaseSplit = splitBySplitIdMap.get("config.it"); + assertThat(itBaseSplit.getApkTargeting()).isEqualTo(apkLanguageTargeting("it")); + assertThat(itBaseSplit.getVariantTargeting()).isEqualTo(fusedSplit.getVariantTargeting()); + assertThat(itBaseSplit.getSplitType()).isEqualTo(SplitType.SYSTEM); + assertThat(itBaseSplit.isMasterSplit()).isFalse(); + assertThat(extractPaths(itBaseSplit.getEntries())) .containsExactly("assets/vr/languages#lang_it/image.jpg"); } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/TextureCompressionFormatAssetsSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/TextureCompressionFormatAssetsSplitterTest.java index 1eb5c908..1d6acd98 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/TextureCompressionFormatAssetsSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/TextureCompressionFormatAssetsSplitterTest.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.splitters; +import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; import static com.android.tools.build.bundletool.model.ManifestMutator.withSplitsRequired; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.compareManifestMutators; @@ -86,7 +87,8 @@ public void multipleTexturesAndDefaultSplit() throws Exception { assertThat(assetsSplits).hasSize(3); List defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); assertThat(defaultSplits).hasSize(1); - assertThat(extractPaths(defaultSplits.get(0).getEntries())).containsExactly("assets/file.txt"); + assertThat(extractPaths(defaultSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/file.txt"); List etc1Splits = getSplitsWithTargetingEqualTo( assetsSplits, @@ -95,7 +97,7 @@ public void multipleTexturesAndDefaultSplit() throws Exception { TextureCompressionFormatAlias.ETC1_RGB8, ImmutableSet.of(TextureCompressionFormatAlias.THREE_DC)))); assertThat(etc1Splits).hasSize(1); - assertThat(extractPaths(etc1Splits.get(0).getEntries())) + assertThat(extractPaths(etc1Splits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) .containsExactly("assets/images#tcf_etc1/image.jpg"); List threeDcSplits = getSplitsWithTargetingEqualTo( @@ -105,7 +107,7 @@ public void multipleTexturesAndDefaultSplit() throws Exception { TextureCompressionFormatAlias.THREE_DC, ImmutableSet.of(TextureCompressionFormatAlias.ETC1_RGB8)))); assertThat(threeDcSplits).hasSize(1); - assertThat(extractPaths(threeDcSplits.get(0).getEntries())) + assertThat(extractPaths(threeDcSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) .containsExactly("assets/images#tcf_3dc/image.jpg"); } 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 7a2bbf50..2c91086d 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java @@ -186,7 +186,7 @@ public static ApkDescription createMasterApkDescription( } public static ApkDescription createApkDescription( - ApkTargeting apkTargeting, Path apkPath, boolean isMasterSplit) { + ApkTargeting apkTargeting, ZipPath apkPath, boolean isMasterSplit) { return ApkDescription.newBuilder() .setPath(apkPath.toString()) .setTargeting(apkTargeting) @@ -216,7 +216,7 @@ public static ApkDescription instantApkDescription(ApkTargeting apkTargeting, Zi /** Creates an instant apk set with the given module name, ApkTargeting, and path for the apk. */ public static ApkSet createInstantApkSet( - String moduleName, ApkTargeting apkTargeting, Path apkPath) { + String moduleName, ApkTargeting apkTargeting, ZipPath apkPath) { return ApkSet.newBuilder() .setModuleMetadata( ModuleMetadata.newBuilder() @@ -231,7 +231,7 @@ public static ApkSet createInstantApkSet( .build(); } - public static ApkSet createStandaloneApkSet(ApkTargeting apkTargeting, Path apkPath) { + public static ApkSet createStandaloneApkSet(ApkTargeting apkTargeting, ZipPath apkPath) { // Note: Standalone APK is represented as a module named "base". return ApkSet.newBuilder() .setModuleMetadata( @@ -245,7 +245,7 @@ public static ApkSet createStandaloneApkSet(ApkTargeting apkTargeting, Path apkP .build(); } - public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, Path apkPath) { + public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, ZipPath apkPath) { // Note: System APK is represented as a module named "base". return ApkSet.newBuilder() .setModuleMetadata( 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 54a82432..8e496f14 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java @@ -54,7 +54,8 @@ public class FakeDevice extends Device { private final String serialNumber; private final ImmutableMap properties; private final Map commandInjections = new HashMap<>(); - private Optional installApksSideEffect = Optional.empty(); + private Optional> installApksSideEffect = Optional.empty(); + private Optional> pushApksSideEffect = Optional.empty(); private static final Joiner COMMA_JOINER = Joiner.on(','); private static final Joiner DASH_JOINER = Joiner.on('-'); private static final Joiner LINE_JOINER = Joiner.on(System.getProperty("line.separator")); @@ -230,10 +231,19 @@ public void installApks(ImmutableList apks, InstallOptions installOptions) installApksSideEffect.ifPresent(val -> val.apply(apks, installOptions)); } - public void setInstallApksSideEffect(SideEffect sideEffect) { + @Override + public void pushApks(ImmutableList apks, PushOptions pushOptions) { + pushApksSideEffect.ifPresent(val -> val.apply(apks, pushOptions)); + } + + public void setInstallApksSideEffect(SideEffect sideEffect) { installApksSideEffect = Optional.of(sideEffect); } + public void setPushApksSideEffect(SideEffect sideEffect) { + pushApksSideEffect = Optional.of(sideEffect); + } + public void clearInstallApksSideEffect() { installApksSideEffect = Optional.empty(); } @@ -251,7 +261,7 @@ String onExecute() } /** Side effect. */ - public interface SideEffect { - void apply(ImmutableList apks, InstallOptions installOptions); + public interface SideEffect { + void apply(ImmutableList apks, T options); } } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzer.java b/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzer.java new file mode 100755 index 00000000..e292cea5 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzer.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.Message; +import java.util.Base64; +import java.util.List; +import java.util.Random; +import java.util.stream.IntStream; + +/** A helper class to generate random protocol buffer messages for tests. */ +public final class ProtoFuzzer { + + private static final Random RAND = new Random(); + private static final int REPEATED_FIELD_LENGTH = 10; + + /** Returns a new proto message with repeated fields randomly shuffled. */ + @SuppressWarnings("unchecked") // Safe by contract of the `build` method of a proto Builder. + public static T shuffleRepeatedFields(T message) { + Message.Builder shuffled = message.toBuilder(); + shuffleRepeatedFields(shuffled); + return (T) shuffled.build(); + } + + private static void shuffleRepeatedFields(Message.Builder shuffled) { + for (FieldDescriptor field : shuffled.getAllFields().keySet()) { + // Shuffle all contained proto messages recursively. + if (field.getJavaType() == JavaType.MESSAGE) { + if (field.isRepeated()) { + IntStream.range(0, shuffled.getRepeatedFieldCount(field)) + .forEach(i -> shuffleRepeatedFields(shuffled.getRepeatedFieldBuilder(field, i))); + } else { + shuffleRepeatedFields(shuffled.getFieldBuilder(field)); + } + } + // Shuffle values of the field itself. + if (field.isRepeated()) { + int len = shuffled.getRepeatedFieldCount(field); + for (int i = 0; i < len - 1; i++) { + swapRepeatedFieldValues(shuffled, field, i, i + RAND.nextInt(len - i)); + } + } + } + } + + private static void swapRepeatedFieldValues( + Message.Builder mutableMsg, FieldDescriptor field, int idx1, int idx2) { + Object value1 = mutableMsg.getRepeatedField(field, idx1); + Object value2 = mutableMsg.getRepeatedField(field, idx2); + mutableMsg.setRepeatedField(field, idx1, value2); + mutableMsg.setRepeatedField(field, idx2, value1); + } + + /** Generates a proto message of the given class with randomly populated fields. */ + @SuppressWarnings("unchecked") // Safe by contract of the `build` method of a proto Builder. + public static T randomProtoMessage(Class messageClazz) { + T prototype = ProtoReflection.getDefaultInstance(messageClazz); + + Message.Builder fuzzed = prototype.toBuilder(); + for (FieldDescriptor field : prototype.getDescriptorForType().getFields()) { + if (field.isRepeated()) { + fuzzed.clearField(field); + IntStream.range(0, REPEATED_FIELD_LENGTH) + .forEach(i -> fuzzed.addRepeatedField(field, fuzzField(field, prototype))); + } else { + fuzzed.setField(field, fuzzField(field, prototype)); + } + } + + return (T) fuzzed.build(); + } + + private static Object fuzzField(FieldDescriptor field, T containingMessage) { + switch (field.getType().getJavaType()) { + case BOOLEAN: + return RAND.nextBoolean(); + case BYTE_STRING: + return randomByteString(); + case DOUBLE: + return RAND.nextDouble(); + case ENUM: + return randomEnum(field.getEnumType()); + case FLOAT: + return RAND.nextFloat(); + case INT: + return RAND.nextInt(); + case LONG: + return RAND.nextLong(); + case STRING: + return randomString(); + case MESSAGE: + return randomProtoMessage( + ProtoReflection.getJavaClassOfMessageField(containingMessage, field)); + } + throw new RuntimeException("Unhandled field type: " + field.getType()); + } + + private static ByteString randomByteString() { + byte[] bytes = new byte[10]; + RAND.nextBytes(bytes); + return ByteString.copyFrom(bytes); + } + + private static Object randomEnum(EnumDescriptor enumDescriptor) { + List enumConstants = enumDescriptor.getValues(); + return enumConstants.get(RAND.nextInt(enumConstants.size())); + } + + private static String randomString() { + return Base64.getEncoder().encodeToString(randomByteString().toByteArray()); + } + + private ProtoFuzzer() {} +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzerTest.java b/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzerTest.java new file mode 100755 index 00000000..298ad84d --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/ProtoFuzzerTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; + +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.VulkanVersion; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.stream.IntStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ProtoFuzzerTest { + + private static final ImmutableList LETTERS_A_TO_Z = + IntStream.range(0, 'z' - 'a' + 1) + .mapToObj(i -> String.valueOf((char) ('a' + i))) + .collect(toImmutableList()); + + @Test + public void shuffleRepeatedFields() { + LanguageTargeting original = + LanguageTargeting.newBuilder() + .addAllValue(LETTERS_A_TO_Z) + .addAllAlternatives(LETTERS_A_TO_Z) + .build(); + + LanguageTargeting shuffled = ProtoFuzzer.shuffleRepeatedFields(original); + + // Values preserved. + assertThat(original.getValueList()).containsExactlyElementsIn(shuffled.getValueList()); + assertThat(original.getAlternativesList()) + .containsExactlyElementsIn(shuffled.getAlternativesList()); + // Order changed + assertThat(LETTERS_A_TO_Z).isNotEqualTo(shuffled.getValueList()); + assertThat(LETTERS_A_TO_Z).isNotEqualTo(shuffled.getAlternativesList()); + } + + @Test + public void randomProtoMessage_nonMessageFieldPopulated() { + VulkanVersion randomProto = ProtoFuzzer.randomProtoMessage(VulkanVersion.class); + + // Values are populated. + // Chose a proto with integer fields to reduce the risk of accidentally generating something + // that appears like the default instance. + assertThat(randomProto).isNotEqualTo(VulkanVersion.getDefaultInstance()); + } + + @Test + public void randomProtoMessage_messageFieldPopulated() { + ApkTargeting randomProto = ProtoFuzzer.randomProtoMessage(ApkTargeting.class); + + assertThat(randomProto.getAbiTargeting()).isNotEqualTo(ApkTargeting.getDefaultInstance()); + } + + @Test + public void randomProtoMessage_repeatedNonMessageFieldPopulated() { + LanguageTargeting randomProto = ProtoFuzzer.randomProtoMessage(LanguageTargeting.class); + + // At least some values populated. + assertThat(randomProto).isNotEqualTo(LanguageTargeting.getDefaultInstance()); + // Not all random values are the same. + assertThat(ImmutableSet.copyOf(randomProto.getValueList()).size()).isGreaterThan(1); + assertThat(ImmutableSet.copyOf(randomProto.getAlternativesList()).size()).isGreaterThan(1); + } + + @Test + public void randomProtoMessage_repeatedMessageFieldPopulated() { + AbiTargeting randomProto = ProtoFuzzer.randomProtoMessage(AbiTargeting.class); + + // At least some values populated. + assertThat(randomProto).isNotEqualTo(AbiTargeting.getDefaultInstance()); + // Not all random values are the same. + assertThat(ImmutableSet.copyOf(randomProto.getValueList()).size()).isGreaterThan(1); + assertThat(ImmutableSet.copyOf(randomProto.getAlternativesList()).size()).isGreaterThan(1); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflection.java b/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflection.java new file mode 100755 index 00000000..35ff5ba9 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflection.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.Message; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; + +/** Utility class for Java reflection on Protocol buffer messages. */ +public final class ProtoReflection { + + /** Invokes the {@code .getDefaultInstance()} of the proto message class. */ + @SuppressWarnings("unchecked") // Safe by contract of the `getDefaultInstance` method. + public static T getDefaultInstance(Class clazz) { + try { + Method method = clazz.getMethod("getDefaultInstance"); + return (T) method.invoke(clazz); + } catch (Exception e) { + throw new RuntimeException("Failed to get default instance for proto: " + clazz.getName(), e); + } + } + + /** + * Gets the {@link Class} object corresponding to the given message field. + * + *

    If the field is repeated, then returns the class of the single item, rather than the + * collection class. + */ + @SuppressWarnings("unchecked") // The unchecked cast is executed for proto message field only. + public static Class getJavaClassOfMessageField( + T message, FieldDescriptor field) { + checkArgument(field.getType().getJavaType().equals(JavaType.MESSAGE)); + + if (field.isRepeated()) { + String fieldGetterName = getterNameForProtoField(field); + try { + Method fieldGetter = message.getClass().getMethod(fieldGetterName); + ParameterizedType fieldTypeArg = (ParameterizedType) fieldGetter.getGenericReturnType(); + checkState( + fieldTypeArg.getActualTypeArguments().length == 1, + "Collection representing a repeated field should have exactly one type argument."); + return (Class) fieldTypeArg.getActualTypeArguments()[0]; + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "Failed to resolve getter of repeated field " + + field.getName() + + " in proto " + + message.getClass().getName(), + e); + } + } else { + return (Class) message.getField(field).getClass(); + } + } + + private static String getterNameForProtoField(FieldDescriptor field) { + String capitalizedFieldName = + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); + return "get" + capitalizedFieldName + (field.isRepeated() ? "List" : ""); + } + + private ProtoReflection() {} +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflectionTest.java b/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflectionTest.java new file mode 100755 index 00000000..9be5dbbc --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/ProtoReflectionTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ProtoReflectionTest { + + @Test + public void getDefaultInstance() { + assertThat(ProtoReflection.getDefaultInstance(ApkTargeting.class)) + .isEqualTo(ApkTargeting.getDefaultInstance()); + } + + @Test + public void getJavaClassOfMessageField_forSimpleMessageField() { + Message proto = ApkTargeting.getDefaultInstance(); + FieldDescriptor field = proto.getDescriptorForType().findFieldByName("abi_targeting"); + + assertThat(ProtoReflection.getJavaClassOfMessageField(proto, field)) + .isEqualTo(AbiTargeting.class); + } + + @Test + public void getJavaClassOfMessageField_forRepeatedMessageField() { + Message proto = AbiTargeting.getDefaultInstance(); + FieldDescriptor field = proto.getDescriptorForType().findFieldByName("value"); + + assertThat(ProtoReflection.getJavaClassOfMessageField(proto, field)).isEqualTo(Abi.class); + } + + @Test + public void getJavaClassOfMessageField_forNonMessageField_throws() { + Message proto = Abi.getDefaultInstance(); + FieldDescriptor field = proto.getDescriptorForType().findFieldByName("alias"); + + assertThrows( + IllegalArgumentException.class, + () -> ProtoReflection.getJavaClassOfMessageField(proto, field)); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java index 22978a05..282ba902 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.junit.jupiter.api.function.Executable; @@ -61,7 +62,15 @@ public static void expectMissingRequiredFlagException(String flag, Executable ru * instances, preserving the order. */ public static ImmutableList extractPaths(ImmutableList entries) { - return entries.stream() + return extractPaths(entries.stream()); + } + + /** + * Returns paths of the given {@link com.android.tools.build.bundletool.model.ModuleEntry} + * instances, preserving the order. + */ + public static ImmutableList extractPaths(Stream entries) { + return entries .map(ModuleEntry::getPath) .map(ZipPath::toString) .collect(toImmutableList()); diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java index d9b28a34..2a6fd4dd 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java @@ -18,9 +18,9 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.apex.ApexManifestProto.ApexManifest; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.TargetedApexImage; import com.android.tools.build.bundletool.model.BundleModule; @@ -35,8 +35,9 @@ @RunWith(JUnit4.class) public class ApexBundleValidatorTest { private static final String PKG_NAME = "com.test.app"; - private static final String APEX_MANIFEST_PATH = "root/apex_manifest.json"; - private static final byte[] APEX_MANIFEST = "{\"name\": \"com.test.app\"}".getBytes(UTF_8); + private static final String APEX_MANIFEST_PATH = "root/apex_manifest.pb"; + private static final byte[] APEX_MANIFEST = + ApexManifest.newBuilder().setName("com.test.app").build().toByteArray(); private static final ApexImages APEX_CONFIG = ApexImages.newBuilder() .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.img")) @@ -98,7 +99,7 @@ public void validateModule_missingPackageFromApexManifest_throws() throws Except new BundleModuleBuilder("apexTestModule") .setManifest(androidManifest(PKG_NAME)) .setApexConfig(APEX_CONFIG) - .addFile(APEX_MANIFEST_PATH, "{}".getBytes(UTF_8)) + .addFile(APEX_MANIFEST_PATH, ApexManifest.getDefaultInstance().toByteArray()) .addFile("apex/x86_64.img") .addFile("apex/x86.img") .addFile("apex/armeabi-v7a.img") diff --git a/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java index 1c995587..a1c4171f 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java @@ -168,6 +168,21 @@ public void optimizations_tcfDimensionSuffixStripping_ok() throws Exception { new BundleConfigValidator().validateBundle(appBundle); } + @Test + public void optimizations_nonTcfDimensionsSuffixStrippingDisabled_ok() throws Exception { + AppBundle appBundle = + createAppBundle( + BundleConfigBuilder.create() + .clearOptimizations() + .addSplitDimension( + SplitDimension.newBuilder() + .setValueValue(Value.LANGUAGE_VALUE) + .setSuffixStripping(SuffixStripping.newBuilder().setEnabled(false)) + .build())); + + new BundleConfigValidator().validateBundle(appBundle); + } + @Test public void optimizations_nonTcfDimensionsSuffixStripping_throws() throws Exception { AppBundle appBundle = @@ -191,7 +206,7 @@ public void optimizations_nonTcfDimensionsSuffixStripping_throws() throws Except } @Test - public void optimizations_tcfDimensionSuffixStrippingWithoutDefault_throws() throws Exception { + public void optimizations_tcfDimensionSuffixStrippingWithoutDefault_ok() throws Exception { AppBundle appBundle = createAppBundle( BundleConfigBuilder.create() @@ -202,12 +217,7 @@ public void optimizations_tcfDimensionSuffixStrippingWithoutDefault_throws() thr .setSuffixStripping(SuffixStripping.newBuilder().setEnabled(true)) .build())); - ValidationException exception = - assertThrows( - ValidationException.class, () -> new BundleConfigValidator().validateBundle(appBundle)); - assertThat(exception) - .hasMessageThat() - .contains("Suffix stripping was enabled without specifying a default suffix."); + new BundleConfigValidator().validateBundle(appBundle); } @Test