diff --git a/README.md b/README.md index b220d693..13e46969 100755 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [0.10.2](https://github.com/google/bundletool/releases) +Latest release: [0.10.3](https://github.com/google/bundletool/releases) diff --git a/build.gradle b/build.gradle index 58411da4..c2a00e4b 100755 --- a/build.gradle +++ b/build.gradle @@ -32,13 +32,13 @@ configurations { // The repackaging rules are defined in the "shadowJar" task below. dependencies { - compile "com.android.tools:r8:1.0.37" + compile "com.android.tools:r8:1.5.68" compile "com.android.tools.build:apkzlib:3.4.0-beta01" compile "com.android.tools.ddms:ddmlib:26.2.0" shadow "com.android.tools.build:aapt2-proto:0.4.0" - shadow "com.google.auto.value:auto-value:1.5.2" - annotationProcessor "com.google.auto.value:auto-value:1.5.2" + shadow "com.google.auto.value:auto-value-annotations:1.6.2" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" shadow "com.google.errorprone:error_prone_annotations:2.3.1" shadow "com.google.guava:guava:27.0.1-jre" shadow "com.google.protobuf:protobuf-java:3.4.0" @@ -49,8 +49,8 @@ dependencies { compileLinux "com.android.tools.build:aapt2:3.5.0-alpha03-5252756:linux" testCompile "com.android.tools.build:aapt2-proto:0.4.0" - testCompile "com.google.auto.value:auto-value-annotations:1.5.2" - testAnnotationProcessor "com.google.auto.value:auto-value:1.5.2" + testCompile "com.google.auto.value:auto-value-annotations:1.6.2" + testAnnotationProcessor "com.google.auto.value:auto-value:1.6.2" testCompile "com.google.errorprone:error_prone_annotations:2.3.1" testCompile "com.google.guava:guava:27.0.1-jre" testCompile "com.google.truth.extensions:truth-java8-extension:0.45" diff --git a/gradle.properties b/gradle.properties index bdc760ff..d87277fe 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 0.10.2 +release_version = 0.10.3 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 32a96c59..8f1bee26 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java index 86503a72..be0079d9 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java @@ -23,7 +23,10 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Devices.DeviceSpec; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; @@ -52,6 +55,7 @@ import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -169,11 +173,22 @@ ImmutableList execute(PrintStream output) { .map( modules -> modules.contains(ALL_MODULES_SHORTCUT) - ? toc.getVariantList().stream() - .flatMap(variant -> variant.getApkSetList().stream()) - .map(apkSet -> apkSet.getModuleMetadata().getName()) + ? Stream.concat( + toc.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .map(apkSet -> apkSet.getModuleMetadata().getName()), + toc.getAssetSliceSetList().stream() + .filter( + sliceSet -> + sliceSet + .getAssetModuleMetadata() + .getDeliveryType() + .equals(DeliveryType.INSTALL_TIME)) + .map(AssetSliceSet::getAssetModuleMetadata) + .map(AssetModuleMetadata::getName)) .collect(toImmutableSet()) : modules); + validateAssetModules(toc, requestedModuleNames); ApkMatcher apkMatcher = new ApkMatcher(getDeviceSpec(), requestedModuleNames, getInstant()); ImmutableList matchedApks = apkMatcher.getMatchingApks(toc); @@ -208,6 +223,33 @@ private void validateInput() { } } + /** Check that none of the requested modules is an asset module that is not install-time. */ + private static void validateAssetModules( + BuildApksResult toc, Optional> requestedModuleNames) { + if (requestedModuleNames.isPresent()) { + ImmutableList requestedNonInstallTimeAssetModules = + toc.getAssetSliceSetList().stream() + .filter( + sliceSet -> + !sliceSet + .getAssetModuleMetadata() + .getDeliveryType() + .equals(DeliveryType.INSTALL_TIME)) + .map(AssetSliceSet::getAssetModuleMetadata) + .map(AssetModuleMetadata::getName) + .filter(requestedModuleNames.get()::contains) + .collect(toImmutableList()); + if (!requestedNonInstallTimeAssetModules.isEmpty()) { + throw ValidationException.builder() + .withMessage( + String.format( + "The following requested asset packs do not have install time delivery: %s.", + requestedNonInstallTimeAssetModules)) + .build(); + } + } + } + private ImmutableList extractMatchedApksFromApksArchive( ImmutableList matchedApkPaths) { Path outputDirectoryPath = @@ -295,10 +337,10 @@ public static CommandHelp help() { .setExampleValue("base,module1,module2") .setOptional(true) .setDescription( - "List of modules to be extracted, or \"%s\" for all modules. Defaults to " - + "modules installed during the first install, i.e. not on-demand. Note " - + "that the dependent modules will also be extracted. The value of this " - + "flag is ignored if the device receives a standalone APK.", + "List of modules to be extracted, or \"%s\" for all modules. " + + "Defaults to modules installed during the first install, i.e. not " + + "on-demand. Note that the dependent modules will also be extracted. The " + + "value of this flag is ignored if the device receives a standalone APK.", ALL_MODULES_SHORTCUT) .build()) .addFlag( 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 1be90abe..b043d3fb 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 @@ -233,10 +233,10 @@ public static CommandHelp help() { .setExampleValue("base,module1,module2") .setOptional(true) .setDescription( - "List of modules to be installed, or \"%s\" for all modules. Defaults to " - + "modules installed during first install, i.e. not on-demand. Note that " - + "the dependent modules will also be installed. The value of this flag is " - + "ignored if the device receives a standalone APK.", + "List of modules to be installed, or \"%s\" for all modules. " + + "Defaults to modules installed during the first install, i.e. not " + + "on-demand. Note that the dependent modules will also be extracted. The " + + "value of this flag is ignored if the device receives a standalone APK.", ExtractApksCommand.ALL_MODULES_SHORTCUT) .build()) .build(); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java index e683cee2..31606898 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java @@ -102,10 +102,10 @@ private void printBundleSummary(AppBundle appBundle) { printModuleSummary(moduleEntry.getValue()); } if (!appBundle.getAssetModules().isEmpty()) { - System.out.printf("Remote asset modules:\n"); + System.out.printf("Asset packs:\n"); for (Entry moduleEntry : appBundle.getAssetModules().entrySet()) { - System.out.printf("\tRemote asset module: %s\n", moduleEntry.getKey()); + System.out.printf("\tAsset pack: %s\n", moduleEntry.getKey()); printModuleSummary(moduleEntry.getValue()); } } 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 aa7c58a0..bbd7a7d5 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 @@ -23,6 +23,8 @@ import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.ModuleMetadata; @@ -96,10 +98,19 @@ public ApkMatcher( public ImmutableList getMatchingApks(BuildApksResult buildApksResult) { Optional matchingVariant = variantMatcher.getMatchingVariant(buildApksResult); - return matchingVariant.isPresent() - ? getMatchingApksFromVariant( - matchingVariant.get(), Version.of(buildApksResult.getBundletool().getVersion())) - : ImmutableList.of(); + if (matchingVariant.isPresent()) { + validateVariant(matchingVariant.get(), buildApksResult); + } + + ImmutableList variantApks = + matchingVariant.isPresent() + ? getMatchingApksFromVariant( + matchingVariant.get(), Version.of(buildApksResult.getBundletool().getVersion())) + : ImmutableList.of(); + + ImmutableList assetModuleApks = getMatchingApksFromAssetModules(buildApksResult); + + return ImmutableList.builder().addAll(variantApks).addAll(assetModuleApks).build(); } public ImmutableList getMatchingApksFromVariant(Variant variant, Version bundleVersion) { @@ -128,8 +139,6 @@ public ImmutableList getMatchingApksFromVariant(Variant variant, Versio private Predicate getModuleNameMatcher(Variant variant, Version bundleVersion) { if (requestedModuleNames.isPresent()) { - validateVariant(variant); - ImmutableMultimap moduleDependenciesMap = buildAdjacencyMap(variant); HashSet dependencyModules = new HashSet<>(requestedModuleNames.get()); @@ -155,15 +164,19 @@ private Predicate getModuleNameMatcher(Variant variant, Version bundleVe } } - private void validateVariant(Variant variant) { + private void validateVariant(Variant variant, BuildApksResult buildApksResult) { if (requestedModuleNames.isPresent()) { - Set unknownModules = - Sets.difference( - requestedModuleNames.get(), + Set availableModules = + Sets.union( variant.getApkSetList().stream() .map(ApkSet::getModuleMetadata) .map(ModuleMetadata::getName) + .collect(toImmutableSet()), + buildApksResult.getAssetSliceSetList().stream() + .map(AssetSliceSet::getAssetModuleMetadata) + .map(AssetModuleMetadata::getName) .collect(toImmutableSet())); + Set unknownModules = Sets.difference(requestedModuleNames.get(), availableModules); if (!unknownModules.isEmpty()) { throw ValidationException.builder() .withMessage( @@ -251,4 +264,42 @@ private void checkCompatibleWithApkTargetingHelper( TargetingDimensionMatcher matcher, ApkTargeting apkTargeting) { matcher.checkDeviceCompatible(matcher.getTargetingValue(apkTargeting)); } + + private ImmutableList getMatchingApksFromAssetModules(BuildApksResult buildApksResult) { + ImmutableList.Builder matchedApksBuilder = ImmutableList.builder(); + + Predicate assetModuleNameMatcher = + getInstallTimeAssetModuleNameMatcher(buildApksResult); + + for (AssetSliceSet sliceSet : buildApksResult.getAssetSliceSetList()) { + String moduleName = sliceSet.getAssetModuleMetadata().getName(); + for (ApkDescription apkDescription : sliceSet.getApkDescriptionList()) { + ApkTargeting apkTargeting = apkDescription.getTargeting(); + + checkCompatibleWithApkTargeting(apkTargeting); + + if (matchesApk(apkTargeting, /*isSplit=*/ true, moduleName, assetModuleNameMatcher)) { + matchedApksBuilder.add(ZipPath.create(apkDescription.getPath())); + } + } + } + return matchedApksBuilder.build(); + } + + private Predicate getInstallTimeAssetModuleNameMatcher(BuildApksResult buildApksResult) { + ImmutableSet upfrontAssetModuleNames = + buildApksResult.getAssetSliceSetList().stream() + .filter( + sliceSet -> + sliceSet + .getAssetModuleMetadata() + .getDeliveryType() + .equals(DeliveryType.INSTALL_TIME)) + .map(sliceSet -> sliceSet.getAssetModuleMetadata().getName()) + .collect(toImmutableSet()); + + return requestedModuleNames.isPresent() + ? Sets.intersection(upfrontAssetModuleNames, requestedModuleNames.get())::contains + : upfrontAssetModuleNames::contains; + } } 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 19f35b4a..fcf4c74d 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 @@ -77,6 +77,9 @@ public abstract class BundleModule { /** 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"); + /** The NOTICE file of an APEX Bundle module. */ + public static final ZipPath APEX_NOTICE_PATH = ZipPath.create("assets/NOTICE.html.gz"); + /** Used to parse file names in the apex/ directory, for multi-Abi targeting. */ public static final Splitter ABI_SPLITTER = Splitter.on(".").omitEmptyStrings(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/DelegatingModuleEntry.java b/src/main/java/com/android/tools/build/bundletool/model/DelegatingModuleEntry.java new file mode 100755 index 00000000..0e92c970 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/DelegatingModuleEntry.java @@ -0,0 +1,63 @@ +/* + * 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; + +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.MustBeClosed; +import java.io.InputStream; + +/** + * Represents a delegate for a ModuleEntry in a an App Bundle's module. + * + *

Useful for selectively overriding certain method(s) while leaving the rest of the + * functionality unchanged. + */ +@Immutable +public class DelegatingModuleEntry implements ModuleEntry { + + private final ModuleEntry delegate; + + public DelegatingModuleEntry(ModuleEntry delegate) { + this.delegate = delegate; + } + + @MustBeClosed + @Override + public InputStream getContent() { + return delegate.getContent(); + } + + @Override + public ZipPath getPath() { + return delegate.getPath(); + } + + @Override + public boolean isDirectory() { + return delegate.isDirectory(); + } + + @Override + public boolean shouldCompress() { + return delegate.shouldCompress(); + } + + @Override + public ModuleEntry setCompression(boolean shouldCompress) { + return delegate.setCompression(shouldCompress); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java index cfcc9c5c..1a633565 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java @@ -315,15 +315,6 @@ public static Optional fromManifestElement( return fromManifestElement(manifestElement, "delivery", isFastFollowAllowed); } - /** - * Returns the instance of the delivery element for instant delivery if Android Manifest contains - * the element. - */ - public static Optional instantFromManifestElement( - XmlProtoElement manifestElement, boolean isFastFollowAllowed) { - return fromManifestElement(manifestElement, "instant-delivery", isFastFollowAllowed); - } - private static Optional fromManifestElement( XmlProtoElement manifestElement, String deliveryTag, boolean isFastFollowAllowed) { return manifestElement @@ -336,6 +327,15 @@ private static Optional fromManifestElement( }); } + /** + * Returns the instance of the delivery element for instant delivery if Android Manifest contains + * the element. + */ + public static Optional instantFromManifestElement( + XmlProtoElement manifestElement, boolean isFastFollowAllowed) { + return fromManifestElement(manifestElement, "instant-delivery", isFastFollowAllowed); + } + @VisibleForTesting static Optional fromManifestRootNode( XmlNode xmlNode, boolean isFastFollowAllowed) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index 42a45464..f7db455d 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -42,6 +42,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.SUPPORTS_GL_TEXTURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_RESOURCE_ID; @@ -95,6 +97,12 @@ public ManifestEditor setMaxSdkVersion(int maxSdkVersion) { MAX_SDK_VERSION_ATTRIBUTE_NAME, MAX_SDK_VERSION_RESOURCE_ID, maxSdkVersion); } + /** Sets the targetSdkVersion attribute. */ + public ManifestEditor setTargetSdkVersion(int targetSdkVersion) { + return setUsesSdkAttribute( + TARGET_SDK_VERSION_ATTRIBUTE_NAME, TARGET_SDK_VERSION_RESOURCE_ID, targetSdkVersion); + } + /** Sets split id and related manifest entries for feature/master split. */ public ManifestEditor setSplitIdForFeatureSplit(String splitId) { if (isBaseSplit(splitId)) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java index 22cf867f..b1e34657 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java @@ -59,6 +59,18 @@ default InputStreamSupplier getContentSupplier() { */ ModuleEntry setCompression(boolean shouldCompress); + /** + * Creates and returns a new ModuleEntry, identical with the old one, but with a different path. + */ + default ModuleEntry setPath(ZipPath newPath) { + return new DelegatingModuleEntry(this) { + @Override + public ZipPath getPath() { + return newPath; + } + }; + } + /** Checks whether the given entries are identical. */ static boolean equal(ModuleEntry entry1, ModuleEntry entry2) { if (!entry1.getPath().equals(entry2.getPath())) { 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 b618baf9..97acfdc2 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 @@ -453,7 +453,7 @@ private static ModuleSplit fromFeatureBundleModule( public static ModuleSplit fromAssetBundleModule(BundleModule bundleModule) { checkArgument( bundleModule.getModuleType().equals(ModuleType.ASSET_MODULE), - "Expected an Asset Module, got %s", + "Expected an asset pack, got %s", bundleModule.getModuleType()); ModuleSplit.Builder splitBuilder = ModuleSplit.builder() 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 8b8c3b26..476501ca 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.10.2"; + private static final String CURRENT_VERSION = "0.10.3"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { 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 6b945d9b..9522d6b4 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 @@ -43,22 +43,27 @@ 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.utils.xmlproto.XmlProtoAttribute; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import java.util.Optional; /** Validates {@code AndroidManifest.xml} file of each module. */ public class AndroidManifestValidator extends SubValidator { private static final int UPFRONT_ASSET_PACK_MIN_SDK_VERSION = 21; + private static final Joiner COMMA_JOINER = Joiner.on(','); @Override public void validateAllModules(ImmutableList modules) { validateSameVersionCode(modules); validateInstant(modules); validateNoVersionCodeInAssetModules(modules); + validateTargetSandboxVersion(modules); + validateMinSdk(modules); } public void validateSameVersionCode(ImmutableList modules) { @@ -76,7 +81,7 @@ public void validateSameVersionCode(ImmutableList modules) { } } - private void validateNoVersionCodeInAssetModules(ImmutableList modules) { + private static void validateNoVersionCodeInAssetModules(ImmutableList modules) { Optional assetModuleWithVersionCode = modules.stream() .filter( @@ -98,6 +103,56 @@ private void validateNoVersionCodeInAssetModules(ImmutableList mod } } + void validateTargetSandboxVersion(ImmutableList modules) { + ImmutableList targetSandboxVersion = + modules.stream() + .map(BundleModule::getAndroidManifest) + .filter(manifest -> !manifest.getModuleType().equals(ModuleType.ASSET_MODULE)) + .map(AndroidManifest::getTargetSandboxVersion) + .filter(Optional::isPresent) + .map(Optional::get) + .distinct() + .sorted() + .collect(toImmutableList()); + + if (targetSandboxVersion.size() > 1) { + throw ValidationException.builder() + .withMessage( + "The attribute 'targetSandboxVersion' should have the same value across modules, but " + + "found [%s]", + COMMA_JOINER.join(targetSandboxVersion)) + .build(); + } else if (targetSandboxVersion.size() == 1 + && Iterables.getOnlyElement(targetSandboxVersion) > 2) { + throw ValidationException.builder() + .withMessage( + "The attribute 'targetSandboxVersion' cannot have a value greater than 2, but found " + + "%d", + Iterables.getOnlyElement(targetSandboxVersion)) + .build(); + } + } + + private static void validateMinSdk(ImmutableList modules) { + int baseMinSdk = + modules.stream() + .filter(BundleModule::isBaseModule) + .map(BundleModule::getAndroidManifest) + .mapToInt(AndroidManifest::getEffectiveMinSdkVersion) + .findFirst() + .orElseThrow(() -> new ValidationException("No base module found.")); + + if (modules.stream() + .filter(m -> m.getAndroidManifest().getMinSdkVersion().isPresent()) + .anyMatch(m -> m.getAndroidManifest().getEffectiveMinSdkVersion() < baseMinSdk)) { + throw ValidationException.builder() + .withMessage( + "Modules cannot have a minSdkVersion attribute with a value lower than " + + "the one from the base module.") + .build(); + } + } + @Override public void validateModule(BundleModule module) { validateInstant(module); @@ -106,7 +161,6 @@ public void validateModule(BundleModule module) { validateFusingConfig(module); validateMinMaxSdk(module); validateNumberOfDistinctSplitIds(module); - validateOnDemandIsInstantMutualExclusion(module); validateAssetModuleManifest(module); validateMinSdkCondition(module); validateNoConditionalTargetingInAssetModules(module); @@ -127,7 +181,7 @@ public void validateBundle(AppBundle appBundle) { } } - private void validateInstant(ImmutableList modules) { + private static void validateInstant(ImmutableList modules) { // If any module is 'instant' validate that 'base' is instant too. BundleModule baseModule = modules.stream() @@ -145,7 +199,7 @@ private void validateInstant(ImmutableList modules) { } } - private void validateInstant(BundleModule module) { + private static void validateInstant(BundleModule module) { AndroidManifest manifest = module.getAndroidManifest(); Optional isInstantModule = manifest.isInstantModule(); if (isInstantModule.orElse(false)) { @@ -158,7 +212,7 @@ private void validateInstant(BundleModule module) { } } - private void validateDeliverySettings(BundleModule module) { + private static void validateDeliverySettings(BundleModule module) { boolean deliveryTypeDeclared = module.getAndroidManifest().isDeliveryTypeDeclared(); ModuleDeliveryType deliveryType = module.getDeliveryType(); @@ -193,7 +247,7 @@ private void validateDeliverySettings(BundleModule module) { } } - private void validateInstantDeliverySettings(BundleModule module) { + private static void validateInstantDeliverySettings(BundleModule module) { if (module.getAndroidManifest().getInstantManifestDeliveryElement().isPresent() && module.getAndroidManifest().getInstantAttribute().isPresent()) { throw ValidationException.builder() @@ -205,28 +259,7 @@ private void validateInstantDeliverySettings(BundleModule module) { } } - private void validateOnDemandIsInstantMutualExclusion(BundleModule module) { - boolean isInstant = module.getAndroidManifest().isInstantModule().orElse(false); - - if (module.getDeliveryType().equals(NO_INITIAL_INSTALL) - && isInstant - && module.getModuleType().equals(ModuleType.FEATURE_MODULE)) { - throw ValidationException.builder() - .withMessage( - "Feature module cannot be on-demand and 'instant' at the same time (module '%s').", - module.getName()) - .build(); - } - if (module.getDeliveryType().equals(CONDITIONAL_INITIAL_INSTALL) && isInstant) { - throw ValidationException.builder() - .withMessage( - "The attribute 'instant' cannot be true for conditional module (module '%s').", - module.getName()) - .build(); - } - } - - private void validateFusingConfig(BundleModule module) { + private static void validateFusingConfig(BundleModule module) { Optional isInstant = module.getAndroidManifest().isInstantModule(); // Skip validations for instant modules. This is only relevant for Pre-L, // where instant apps are not available @@ -248,7 +281,7 @@ private void validateFusingConfig(BundleModule module) { } } - private void validateMinMaxSdk(BundleModule module) { + private static void validateMinMaxSdk(BundleModule module) { AndroidManifest manifest = module.getAndroidManifest(); Optional maxSdk = manifest.getMaxSdkVersion(); Optional minSdk = manifest.getMinSdkVersion(); @@ -272,7 +305,7 @@ private void validateMinMaxSdk(BundleModule module) { } } - private void validateNumberOfDistinctSplitIds(BundleModule module) { + private static void validateNumberOfDistinctSplitIds(BundleModule module) { ImmutableSet splitIds = module .getAndroidManifest() @@ -289,7 +322,7 @@ private void validateNumberOfDistinctSplitIds(BundleModule module) { } } - private void validateAssetModuleManifest(BundleModule module) { + private static void validateAssetModuleManifest(BundleModule module) { ImmutableMultimap allowedManifestElementChildren = ImmutableMultimap.of(DISTRIBUTION_NAMESPACE_URI, "module", NO_NAMESPACE_URI, "uses-split"); @@ -313,7 +346,7 @@ private void validateAssetModuleManifest(BundleModule module) { } /** Validates that if min-sdk condition is present it is >= than the effective minSdk version. */ - private void validateMinSdkCondition(BundleModule module) { + private static void validateMinSdkCondition(BundleModule module) { int effectiveMinSdkVersion = module.getAndroidManifest().getEffectiveMinSdkVersion(); Optional minSdkCondition = module @@ -332,7 +365,7 @@ private void validateMinSdkCondition(BundleModule module) { } } - private void validateNoConditionalTargetingInAssetModules(BundleModule module) { + private static void validateNoConditionalTargetingInAssetModules(BundleModule module) { if (module.getModuleType().equals(ModuleType.ASSET_MODULE) && !module .getModuleMetadata() @@ -346,7 +379,7 @@ private void validateNoConditionalTargetingInAssetModules(BundleModule module) { } } - private void validateInstantAndPersistentDeliveryCombinationsForAssetModules( + private static void validateInstantAndPersistentDeliveryCombinationsForAssetModules( BundleModule module) { if (!module.getAndroidManifest().getModuleType().equals(ModuleType.ASSET_MODULE) || !module.isInstantModule()) { 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 c5ffc9bd..5d253706 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 @@ -23,6 +23,7 @@ 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_PATH; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_NOTICE_PATH; import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.bundle.Files.ApexImages; @@ -48,6 +49,9 @@ /** Validates an APEX bundle. */ public class ApexBundleValidator extends SubValidator { + private static final ImmutableList ALLOWED_APEX_FILES_OUTSIDE_APEX_DIRECTORY = + ImmutableList.of(APEX_MANIFEST_PATH, APEX_NOTICE_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 = ImmutableSet.of( @@ -100,7 +104,7 @@ public void validateModule(BundleModule module) { if (path.startsWith(APEX_DIRECTORY)) { apexImagesBuilder.add(path.toString()); apexFileNamesBuilder.add(path.getFileName().toString()); - } else if (!path.equals(APEX_MANIFEST_PATH)) { + } else if (!ALLOWED_APEX_FILES_OUTSIDE_APEX_DIRECTORY.contains(path)) { throw ValidationException.builder() .withMessage("Unexpected file in APEX bundle: '%s'.", entry.getPath()) .build(); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidator.java index bdf0cae3..263a545a 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidator.java @@ -61,7 +61,7 @@ public void validateAllModules(ImmutableList modules) { checkInstantModuleDependencies(moduleDependenciesMap, modulesByName); checkValidModuleDeliveryTypeDependencies(moduleDependenciesMap, modulesByName); checkMinSdkIsCompatibleWithDependencies(moduleDependenciesMap, modulesByName); - checkNoDependenciesBetweenModulesOfDifferentTypes(moduleDependenciesMap, modulesByName); + checkAssetModulesHaveNoDependencies(moduleDependenciesMap, modulesByName); } private static void checkHasBaseModule(ImmutableList modules) { @@ -252,7 +252,7 @@ private static void checkMinSdkIsCompatibleWithDependencies( } } - private static void checkNoDependenciesBetweenModulesOfDifferentTypes( + private static void checkAssetModulesHaveNoDependencies( Multimap moduleDependenciesMap, ImmutableMap modulesByName) { for (Entry dependencyEntry : moduleDependenciesMap.entries()) { @@ -260,10 +260,12 @@ private static void checkNoDependenciesBetweenModulesOfDifferentTypes( String moduleDepName = dependencyEntry.getValue(); ModuleType moduleType = modulesByName.get(moduleName).getModuleType(); ModuleType moduleDepType = modulesByName.get(moduleDepName).getModuleType(); - if (!moduleDepName.equals(BASE_MODULE_NAME.getName()) && !moduleType.equals(moduleDepType)) { + if (!moduleDepName.equals(BASE_MODULE_NAME.getName()) + && (moduleType.equals(ModuleType.ASSET_MODULE) + || moduleDepType.equals(ModuleType.ASSET_MODULE))) { throw ValidationException.builder() .withMessage( - "Module '%s' cannot depend on module '%s' because the module types are different.", + "Module '%s' cannot depend on module '%s' because one of them is an asset pack.", moduleName, moduleDepName) .build(); } diff --git a/src/main/proto/app_dependencies.proto b/src/main/proto/app_dependencies.proto index 9185beef..9926edce 100755 --- a/src/main/proto/app_dependencies.proto +++ b/src/main/proto/app_dependencies.proto @@ -6,42 +6,51 @@ option java_package = "com.android.bundle"; // Lists the dependencies of an application. message AppDependencies { - // List of all the dependencies, direct and indirect. - repeated Library library = 1; + // List of all the dependencies, direct and indirect. + repeated Library library = 1; - // Dependencies of the libraries from the list above. - repeated LibraryDependencies library_dependencies = 2; + // Dependencies of the libraries from the list above. + repeated LibraryDependencies library_dependencies = 2; - // List of direct dependencies per bundle module. - repeated ModuleDependencies module_dependencies = 3; + // List of direct dependencies per bundle module. + repeated ModuleDependencies module_dependencies = 3; } // List of dependencies of a given library. message LibraryDependencies { - // Indices correspond to the pool of libraries defined in AppDependencies. - optional int32 library_index = 1; - repeated int32 library_dep_index = 2; + // Indices correspond to the pool of libraries defined in AppDependencies. + optional int32 library_index = 1; + repeated int32 library_dep_index = 2; } // Lists the dependencies of a given module. message ModuleDependencies { - optional string module_name = 1; - // Direct module dependencies. - // Index is from the pool of libraries defined in AppDependencies. - repeated int32 dependency_index = 2; + optional string module_name = 1; + // Direct module dependencies. + // Index is from the pool of libraries defined in AppDependencies. + repeated int32 dependency_index = 2; } message Library { - // Oneof allows for support of other library identification systems in the future. - oneof library_oneof { - MavenLibrary maven_library = 1; - } + // Oneof allows for support of other library identification systems in the + // future. + oneof library_oneof { + MavenLibrary maven_library = 1; + } + + // This message contains various digests of the library contents. + message Digests { + // SHA256 hash value of the file contents. + optional bytes sha256 = 1; + } + + optional Digests digests = 2; } message MavenLibrary { - optional string group_id = 1; - optional string artifact_id = 2; - optional string packaging = 3; - optional string classifier = 4; - optional string version = 5; + optional string group_id = 1; + optional string artifact_id = 2; + optional string packaging = 3; + optional string classifier = 4; + optional string version = 5; } 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 0545230b..94e94079 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 @@ -32,16 +32,19 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariantForSingleSplitApk; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.multiAbiTargetingApexVariant; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.splitApkDescription; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.createDeviceSpecFile; import static com.android.tools.build.bundletool.testing.DeviceFactory.density; import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceFeatures; import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceWithSdk; +import static com.android.tools.build.bundletool.testing.DeviceFactory.lDeviceWithLocales; import static com.android.tools.build.bundletool.testing.DeviceFactory.locales; import static com.android.tools.build.bundletool.testing.DeviceFactory.mergeSpecs; import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkVersion; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeModuleTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleFeatureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleMinSdkVersionTargeting; @@ -53,6 +56,8 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Config.Bundletool; 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 5496b9c7..205c3210 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 @@ -23,6 +23,7 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSplitApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariantForSingleSplitApk; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.splitApkDescription; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.density; import static com.android.tools.build.bundletool.testing.DeviceFactory.lDeviceWithLocales; @@ -32,6 +33,7 @@ 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.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; @@ -39,6 +41,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Config.Bundletool; @@ -695,6 +699,84 @@ public void moduleDependencies_diamondGraph( "feature4-master.apk"); } + @Test + @Theory + public void extractAssetModules(@FromDataPoints("apksInDirectory") boolean apksInDirectory) + throws Exception { + String installTimeModule1 = "installtime_assetmodule1"; + String installTimeModule2 = "installtime_assetmodule2"; + String onDemandModule = "ondemand_assetmodule"; + ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); + ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); + ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); + ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); + ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + ZipPath baseApk = ZipPath.create("base-master.apk"); + BuildApksResult tableOfContent = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule1) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule2) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(onDemandModule) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) + .build(); + + Path apksFile = createApks(tableOfContent, apksInDirectory); + + List installedApks = 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)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .setModules(ImmutableSet.of(installTimeModule1)) + .build() + .execute(); + + assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + .containsExactly( + baseApk.toString(), installTimeMasterApk1.toString(), installTimeEnApk1.toString()); + } + @Test public void printHelp_doesNotCrash() { GetDeviceSpecCommand.help(); diff --git a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java index 93a3f2c7..2fe7e4a9 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java @@ -63,6 +63,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.ApkSet; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.Variant; @@ -1246,6 +1248,78 @@ public void matchesModuleSplit_incompatibleDeviceThrows() { + "app ABIs: [arm64-v8a, x86]"); } + @Test + public void assetModuleMatch() { + String installTimeModule1 = "installtime_assetmodule1"; + String installTimeModule2 = "installtime_assetmodule2"; + String onDemandModule = "ondemand_assetmodule"; + ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); + ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); + ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); + ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); + ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + ZipPath baseApk = ZipPath.create("base-master.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + oneApkSplitApkVariant( + variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(sdkVersionFrom(1))), + ApkTargeting.getDefaultInstance(), + baseApk)) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule1) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule2) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(onDemandModule) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) + .build(); + + DeviceSpec enDevice = lDeviceWithLocales("en"); + assertThat(new ApkMatcher(enDevice).getMatchingApks(buildApksResult)) + .containsExactly( + baseApk, + installTimeMasterApk1, + installTimeEnApk1, + installTimeMasterApk2, + installTimeEnApk2); + + DeviceSpec esDevice = lDeviceWithLocales("fr"); + assertThat(new ApkMatcher(esDevice).getMatchingApks(buildApksResult)) + .containsExactly(baseApk, installTimeMasterApk1, installTimeMasterApk2); + + assertThat( + new ApkMatcher(enDevice, Optional.of(ImmutableSet.of(installTimeModule1)), false) + .getMatchingApks(buildApksResult)) + .containsExactly(baseApk, installTimeMasterApk1, installTimeEnApk1); + } + private static BuildApksResult buildApksResult(Variant... variants) { return buildApksResult(BundleToolVersion.getCurrentVersion().toString(), variants); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index be91d1cd..8b700d68 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -30,6 +30,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_CODE_RESOURCE_ID; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; @@ -147,6 +148,40 @@ public void setMinSdkVersion_existingAttribute_adjusted() throws Exception { 123)))); } + @Test + public void setTargetSdkVersion_existingAttribute_adjusted() throws Exception { + AndroidManifest androidManifest = + AndroidManifest.create( + xmlNode( + xmlElement( + "manifest", + xmlNode( + xmlElement( + "uses-sdk", + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, + "targetSdkVersion", + TARGET_SDK_VERSION_RESOURCE_ID, + 1)))))); + + AndroidManifest editedManifest = androidManifest.toEditor().setTargetSdkVersion(123).save(); + + XmlNode editedManifestRoot = editedManifest.getManifestRoot().getProto(); + assertThat(editedManifestRoot.hasElement()).isTrue(); + XmlElement manifestElement = editedManifestRoot.getElement(); + assertThat(manifestElement.getName()).isEqualTo("manifest"); + assertThat(manifestElement.getChildList()) + .containsExactly( + xmlNode( + xmlElement( + "uses-sdk", + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, + "targetSdkVersion", + TARGET_SDK_VERSION_RESOURCE_ID, + 123)))); + } + @Test public void doNotSetFeatureSplit_forBaseSplit() throws Exception { AndroidManifest androidManifest = 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 d7343270..7a2bbf50 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 @@ -46,6 +46,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.stream.Stream; /** Helpers related to creating APKs archives in tests. */ public final class ApksArchiveHelpers { @@ -55,9 +56,7 @@ public final class ApksArchiveHelpers { public static Path createApksArchiveFile(BuildApksResult result, Path location) throws Exception { ZipBuilder archiveBuilder = new ZipBuilder(); - result.getVariantList().stream() - .flatMap(variant -> variant.getApkSetList().stream()) - .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) + apkDescriptionStream(result) .forEach( apkDesc -> archiveBuilder.addFileWithContent(ZipPath.create(apkDesc.getPath()), DUMMY_BYTES)); @@ -68,10 +67,7 @@ public static Path createApksArchiveFile(BuildApksResult result, Path location) public static Path createApksDirectory(BuildApksResult result, Path location) throws Exception { ImmutableList apkDescriptions = - result.getVariantList().stream() - .flatMap(variant -> variant.getApkSetList().stream()) - .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) - .collect(toImmutableList()); + apkDescriptionStream(result).collect(toImmutableList()); for (ApkDescription apkDescription : apkDescriptions) { Path apkPath = location.resolve(apkDescription.getPath()); @@ -264,4 +260,13 @@ public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, Path apkPath) .setSystemApkType(SystemApkType.SYSTEM))) .build(); } + + private static Stream apkDescriptionStream(BuildApksResult buildApksResult) { + return Stream.concat( + buildApksResult.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()), + buildApksResult.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream())); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java index 55625c85..c201f43b 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java @@ -32,6 +32,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandAttribute; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSandboxVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withVersionCode; import static com.google.common.truth.Truth.assertThat; @@ -342,49 +343,6 @@ public void nonBase_withConditionsAndOnDemandAttribute_throws() throws Exception FEATURE_MODULE_NAME)); } - @Test - public void onDemandAndInstantAttributeSetToTrue_throws() throws Exception { - BundleModule module = - new BundleModuleBuilder(FEATURE_MODULE_NAME) - .setManifest( - androidManifest( - PKG_NAME, - withOnDemandAttribute(true), - withInstant(true), - withFusingAttribute(true))) - .build(); - - ValidationException exception = - assertThrows( - ValidationException.class, () -> new AndroidManifestValidator().validateModule(module)); - assertThat(exception) - .hasMessageThat() - .contains( - String.format( - "Feature module cannot be on-demand and 'instant' at the same time (module '%s').", - FEATURE_MODULE_NAME)); - } - - @Test - public void onDemandElementAndInstantAttributeSetToTrue_throws() throws Exception { - BundleModule module = - new BundleModuleBuilder(FEATURE_MODULE_NAME) - .setManifest( - androidManifest( - PKG_NAME, withOnDemandDelivery(), withInstant(true), withFusingAttribute(true))) - .build(); - - ValidationException exception = - assertThrows( - ValidationException.class, () -> new AndroidManifestValidator().validateModule(module)); - assertThat(exception) - .hasMessageThat() - .contains( - String.format( - "Feature module cannot be on-demand and 'instant' at the same time (module '%s').", - FEATURE_MODULE_NAME)); - } - @Test public void onDemandSetToFalseAndInstantAttributeSetToTrue_ok() throws Exception { BundleModule module = @@ -434,26 +392,80 @@ public void installTimeAndOnDemandDeliveryAndInstantAttributeSetToTrue_ok() thro } @Test - public void moduleConditionsSetAndInstantAttributeTrue_throws() throws Exception { + public void withCorrectTargetSandboxVersionCode_ok() throws Exception { + BundleModule module = baseModule(withTargetSandboxVersion(2)); + + new AndroidManifestValidator().validateAllModules(ImmutableList.of(module)); + } + + @Test + public void withHighTargetSandboxVersionCode_throws() throws Exception { + BundleModule module = baseModule(withTargetSandboxVersion(3)); + + ValidationException e = + assertThrows( + ValidationException.class, + () -> new AndroidManifestValidator().validateAllModules(ImmutableList.of(module))); + + assertThat(e).hasMessageThat().contains("cannot have a value greater than 2, but found 3"); + } + + @Test + public void withMinSdkLowerThanBase_throws() throws Exception { + BundleModule base = baseModule(withMinSdkVersion(20)); BundleModule module = new BundleModuleBuilder(FEATURE_MODULE_NAME) - .setManifest( - androidManifest( - PKG_NAME, - withFeatureCondition("com.android.feature"), - withInstant(true), - withFusingAttribute(true))) + .setManifest(androidManifest(PKG_NAME, withMinSdkVersion(19))) .build(); - ValidationException exception = + ValidationException e = assertThrows( - ValidationException.class, () -> new AndroidManifestValidator().validateModule(module)); - assertThat(exception) + ValidationException.class, + () -> + new AndroidManifestValidator().validateAllModules(ImmutableList.of(base, module))); + + assertThat(e) .hasMessageThat() .contains( - String.format( - "The attribute 'instant' cannot be true for conditional module" + " (module '%s').", - FEATURE_MODULE_NAME)); + "cannot have a minSdkVersion attribute with a value lower than the one from the base" + + " module"); + } + + @Test + public void withMinSdkEqualThanBase_ok() throws Exception { + BundleModule base = baseModule(withMinSdkVersion(20)); + BundleModule module = + new BundleModuleBuilder(FEATURE_MODULE_NAME) + .setManifest(androidManifest(PKG_NAME, withMinSdkVersion(20))) + .build(); + + new AndroidManifestValidator().validateAllModules(ImmutableList.of(base, module)); + + // No exception thrown. + } + + @Test + public void withMinSdkUndeclared_ok() throws Exception { + BundleModule base = baseModule(withMinSdkVersion(20)); + BundleModule module = + new BundleModuleBuilder(FEATURE_MODULE_NAME).setManifest(androidManifest(PKG_NAME)).build(); + + new AndroidManifestValidator().validateAllModules(ImmutableList.of(base, module)); + + // No exception thrown. + } + + @Test + public void withMinSdkHigherThanBase_ok() throws Exception { + BundleModule base = baseModule(withMinSdkVersion(20)); + BundleModule module = + new BundleModuleBuilder(FEATURE_MODULE_NAME) + .setManifest(androidManifest(PKG_NAME, withMinSdkVersion(21))) + .build(); + + new AndroidManifestValidator().validateAllModules(ImmutableList.of(base, module)); + + // No exception thrown. } @Test @@ -677,6 +689,26 @@ public void bundleModules_differentVersionCode_throws() throws Exception { .contains("App Bundle modules should have the same version code but found [2,3]"); } + @Test + public void bundleModules_differentTargetSandboxVersionCode_throws() throws Exception { + ImmutableList bundleModules = + ImmutableList.of( + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest(androidManifest("com.test", withTargetSandboxVersion(1))) + .build(), + new BundleModuleBuilder(FEATURE_MODULE_NAME) + .setManifest(androidManifest("com.test", withTargetSandboxVersion(2))) + .build()); + + ValidationException exception = + assertThrows( + ValidationException.class, + () -> new AndroidManifestValidator().validateAllModules(bundleModules)); + assertThat(exception) + .hasMessageThat() + .contains("should have the same value across modules, but found [1,2]"); + } + @Test public void assetModule_noApplication_ok() throws Exception { BundleModule module = diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidatorTest.java index 27907c09..931f3cfd 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ModuleDependencyValidatorTest.java @@ -494,7 +494,7 @@ public void validateAllModules_onDemandOnly_dependsOnConditional_throws() throws } @Test - public void validateAllModules_assetModuleDependsOnFeatureModule_throws() throws Exception { + public void validateAllModules_assetModuleHasDependency_throws() throws Exception { ImmutableList allModules = ImmutableList.of( module("base", androidManifest(PKG_NAME)), @@ -511,12 +511,12 @@ PKG_NAME, withOnDemandDelivery(), withUsesSplit("feature"))), assertThat(exception) .hasMessageThat() .contains( - "Module 'asset' cannot depend on module 'feature' because the module types are" - + " different."); + "Module 'asset' cannot depend on module 'feature' because one of them is an asset" + + " pack."); } @Test - public void validateAllModules_featureModuleDependsOnAssetModule_throws() throws Exception { + public void validateAllModules_assetModuleHasDependee_throws() throws Exception { ImmutableList allModules = ImmutableList.of( module("base", androidManifest(PKG_NAME)), @@ -532,8 +532,8 @@ public void validateAllModules_featureModuleDependsOnAssetModule_throws() throws assertThat(exception) .hasMessageThat() .contains( - "Module 'feature' cannot depend on module 'asset' because the module types are" - + " different."); + "Module 'feature' cannot depend on module 'asset' because one of them is an asset" + + " pack."); } @Test