diff --git a/README.md b/README.md index 32067aca..01dfb124 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.15.4](https://github.com/google/bundletool/releases) +Latest release: [1.15.5](https://github.com/google/bundletool/releases) diff --git a/build.gradle b/build.gradle index 3776af93..83535794 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ dependencies { 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:31.0.1-jre" + shadow "com.google.guava:guava:31.1-jre" shadow "com.google.protobuf:protobuf-java:3.19.2" shadow "com.google.protobuf:protobuf-java-util:3.19.2" shadow "com.google.dagger:dagger:2.28.3" @@ -64,7 +64,7 @@ dependencies { testImplementation "com.google.auto.value:auto-value-annotations:1.6.2" testAnnotationProcessor "com.google.auto.value:auto-value:1.6.2" testImplementation "com.google.errorprone:error_prone_annotations:2.3.1" - testImplementation "com.google.guava:guava:31.0.1-jre" + testImplementation "com.google.guava:guava:31.1-jre" testImplementation "com.google.truth.extensions:truth-java8-extension:0.45" testImplementation "com.google.truth.extensions:truth-proto-extension:0.45" testImplementation "com.google.jimfs:jimfs:1.1" diff --git a/gradle.properties b/gradle.properties index 066fd510..f1be7d20 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.15.4 +release_version = 1.15.5 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 f4157a50..4c3b3082 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -45,6 +45,7 @@ import com.android.tools.build.bundletool.model.ModuleDeliveryType; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.ResourceId; import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.targeting.AlternativeVariantTargetingPopulator; @@ -326,10 +327,8 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat apkGenerationConfiguration.setEnableUncompressedNativeLibraries( apkOptimizations.getUncompressNativeLibraries()); - apkGenerationConfiguration.setEnableDexCompressionSplitter( - getEnableUncompressedDexOptimization(appBundle)); - apkGenerationConfiguration.setDexCompressionSplitterForTargetSdk( - apkOptimizations.getUncompressedDexTargetSdk()); + setEnableUncompressedDexOptimization(appBundle, apkGenerationConfiguration); + apkGenerationConfiguration.setEnableSparseEncodingVariant( bundleConfig .getOptimizations() @@ -347,10 +346,13 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat installLocation.equals("auto") || installLocation.equals("preferExternal")) .orElse(false)); - apkGenerationConfiguration.setMasterPinnedResourceIds(appBundle.getMasterPinnedResourceIds()); + apkGenerationConfiguration.setMasterPinnedResourceIds( + bundleConfig.getMasterResources().getResourceIdsList().stream() + .map(ResourceId::create) + .collect(toImmutableSet())); apkGenerationConfiguration.setMasterPinnedResourceNames( - appBundle.getMasterPinnedResourceNames()); + ImmutableSet.copyOf(bundleConfig.getMasterResources().getResourceNamesList())); apkGenerationConfiguration.setSuffixStrippings(apkOptimizations.getSuffixStrippings()); @@ -361,21 +363,18 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat .getMinSdkForAdditionalVariantWithV3Rotation() .ifPresent(apkGenerationConfiguration::setMinSdkForAdditionalVariantWithV3Rotation); + return apkGenerationConfiguration; } - private boolean getEnableUncompressedDexOptimization(AppBundle appBundle) { + private void setEnableUncompressedDexOptimization( + AppBundle appBundle, ApkGenerationConfiguration.Builder builder) { if (appBundle.getUncompressedDexOptOut()) { - return false; + builder.setEnableDexCompressionSplitter(false); + return; } - if (appBundle.getBundleConfig().getOptimizations().hasUncompressDexFiles()) { - // If uncompressed dex is specified in the BundleConfig it will be honoured. - return appBundle.getBundleConfig().getOptimizations().getUncompressDexFiles().getEnabled(); - } - // This is the default value of the optimisation. Depends on the bundletool version. - boolean enableUncompressedDexOptimization = apkOptimizations.getUncompressDexFiles(); - - return enableUncompressedDexOptimization; + builder.setEnableDexCompressionSplitter(apkOptimizations.getUncompressDexFiles()); + builder.setDexCompressionSplitterForTargetSdk(apkOptimizations.getUncompressedDexTargetSdk()); } private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java index 466a4443..2958d3b3 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java @@ -22,6 +22,7 @@ import static com.android.tools.build.bundletool.model.utils.BundleParser.getModulesZip; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import com.android.tools.build.bundletool.androidtools.Aapt2Command; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; @@ -32,6 +33,7 @@ import com.android.tools.build.bundletool.model.ApkListener; import com.android.tools.build.bundletool.model.ApkModifier; import com.android.tools.build.bundletool.model.Password; +import com.android.tools.build.bundletool.model.SdkAsar; import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.SignerConfig; import com.android.tools.build.bundletool.model.SigningConfiguration; @@ -40,6 +42,7 @@ import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; +import com.android.tools.build.bundletool.validation.SdkAsarValidator; import com.android.tools.build.bundletool.validation.SdkBundleValidator; import com.google.auto.value.AutoValue; import com.google.common.util.concurrent.ListeningExecutorService; @@ -72,6 +75,7 @@ public enum OutputFormat { } private static final Flag SDK_BUNDLE_LOCATION_FLAG = Flag.path("sdk-bundle"); + private static final Flag SDK_ARCHIVE_LOCATION_FLAG = Flag.path("sdk-archive"); private static final Flag VERSION_CODE_FLAG = Flag.positiveInteger("version-code"); private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); private static final Flag OUTPUT_FORMAT_FLAG = @@ -90,7 +94,9 @@ public enum OutputFormat { private static final SystemEnvironmentProvider DEFAULT_PROVIDER = new DefaultSystemEnvironmentProvider(); - abstract Path getSdkBundlePath(); + abstract Optional getSdkBundlePath(); + + abstract Optional getSdkArchivePath(); abstract Integer getVersionCode(); @@ -120,6 +126,7 @@ ListeningExecutorService getExecutorService() { public abstract Optional getFirstVariantNumber(); + /** Creates a builder for the {@link BuildSdkApksCommand} with some default settings. */ public static BuildSdkApksCommand.Builder builder() { return new AutoValue_BuildSdkApksCommand.Builder() @@ -135,6 +142,9 @@ public abstract static class Builder { /** Sets the path to the input SDK bundle. Must have the extension ".asb". */ public abstract Builder setSdkBundlePath(Path sdkBundlePath); + /** Sets the path to the input SDK archive. Must have the extension ".asar". */ + public abstract Builder setSdkArchivePath(Path sdkArchivePath); + /** Sets the SDK version code */ public abstract Builder setVersionCode(Integer versionCode); @@ -220,6 +230,7 @@ public Builder setExecutorService(ListeningExecutorService executorService) { */ public abstract Builder setFirstVariantNumber(int firstVariantNumber); + abstract BuildSdkApksCommand autoBuild(); /** @@ -232,7 +243,11 @@ public BuildSdkApksCommand build() { setExecutorServiceInternal(createInternalExecutorService(DEFAULT_THREAD_POOL_SIZE)); setExecutorServiceCreatedByBundleTool(true); } - return autoBuild(); + BuildSdkApksCommand command = autoBuild(); + checkState( + command.getSdkBundlePath().isPresent() ^ command.getSdkArchivePath().isPresent(), + "One and only one of SdkBundlePath and SdkArchivePath should be set."); + return command; } } @@ -244,10 +259,11 @@ static BuildSdkApksCommand fromFlags( ParsedFlags flags, PrintStream out, SystemEnvironmentProvider provider) { Builder sdkApksCommandBuilder = BuildSdkApksCommand.builder() - .setSdkBundlePath(SDK_BUNDLE_LOCATION_FLAG.getRequiredValue(flags)) .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); // Optional arguments. + SDK_BUNDLE_LOCATION_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setSdkBundlePath); + SDK_ARCHIVE_LOCATION_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setSdkArchivePath); OUTPUT_FORMAT_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setOutputFormat); OVERWRITE_OUTPUT_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setOverwriteOutput); VERSION_CODE_FLAG.getValue(flags).ifPresent(sdkApksCommandBuilder::setVersionCode); @@ -275,8 +291,19 @@ static BuildSdkApksCommand fromFlags( public Path execute() { validateInput(); + if (getSdkBundlePath().isPresent()) { + executeForSdkBundle(); + } else if (getSdkArchivePath().isPresent()) { + executeForSdkArchive(); + } else { + throw new IllegalStateException( + "One and only one of SdkBundlePath and SdkArchivePath should be set."); + } + return getOutputFile(); + } - try (ZipFile bundleZip = new ZipFile(getSdkBundlePath().toFile()); + private void executeForSdkBundle() { + try (ZipFile bundleZip = new ZipFile(getSdkBundlePath().get().toFile()); TempDirectory tempDir = new TempDirectory(getClass().getSimpleName())) { SdkBundleValidator bundleValidator = SdkBundleValidator.create(); @@ -287,14 +314,7 @@ public Path execute() { bundleValidator.validateModulesFile(modulesZip); SdkBundle sdkBundle = SdkBundle.buildFromZip(bundleZip, modulesZip, getVersionCode()); bundleValidator.validate(sdkBundle); - - DaggerBuildSdkApksManagerComponent.builder() - .setBuildSdkApksCommand(this) - .setTempDirectory(tempDir) - .setSdkBundle(sdkBundle) - .build() - .create() - .execute(); + executeBuildSdkApksManager(sdkBundle, tempDir); } } catch (ZipException e) { throw InvalidBundleException.builder() @@ -302,19 +322,56 @@ public Path execute() { .withUserMessage("The SDK Bundle is not a valid zip file.") .build(); } catch (IOException e) { - throw new UncheckedIOException("An error occurred when validating the Sdk Bundle.", e); + throw new UncheckedIOException("An error occurred when processing the Sdk Bundle.", e); } finally { if (isExecutorServiceCreatedByBundleTool()) { getExecutorService().shutdown(); } } + } - return getOutputFile(); + private void executeForSdkArchive() { + try (ZipFile sdkAsarZip = new ZipFile(getSdkArchivePath().get().toFile()); + TempDirectory tempDir = new TempDirectory(getClass().getSimpleName())) { + + SdkAsarValidator.validateFile(sdkAsarZip); + + Path modulesPath = tempDir.getPath().resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + try (ZipFile modulesZip = getModulesZip(sdkAsarZip, modulesPath)) { + SdkAsarValidator.validateModulesFile(modulesZip); + SdkAsar sdkAsar = SdkAsar.buildFromZip(sdkAsarZip, modulesZip, modulesPath); + SdkBundle sdkBundle = SdkBundle.buildFromAsar(sdkAsar, getVersionCode()); + SdkBundleValidator.create().validate(sdkBundle); + executeBuildSdkApksManager(sdkBundle, tempDir); + } + } catch (ZipException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage("The SDK archive is not a valid zip file.") + .build(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when processing the Sdk archive.", e); + } finally { + if (isExecutorServiceCreatedByBundleTool()) { + getExecutorService().shutdown(); + } + } + } + + private void executeBuildSdkApksManager(SdkBundle sdkBundle, TempDirectory tempDir) + throws IOException { + DaggerBuildSdkApksManagerComponent.builder() + .setBuildSdkApksCommand(this) + .setTempDirectory(tempDir) + .setSdkBundle(sdkBundle) + .build() + .create() + .execute(); } private void validateInput() { - FilePreconditions.checkFileExistsAndReadable(getSdkBundlePath()); - FilePreconditions.checkFileHasExtension("ASB file", getSdkBundlePath(), ".asb"); + getSdkBundlePath().ifPresent(BuildSdkApksCommand::validateSdkBundlePath); + getSdkArchivePath().ifPresent(BuildSdkApksCommand::validateSdkArchivePath); switch (getOutputFormat()) { case APK_SET: @@ -344,6 +401,16 @@ private static ListeningExecutorService createInternalExecutorService(int maxThr return MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(maxThreads)); } + private static void validateSdkBundlePath(Path sdkBundlePath) { + FilePreconditions.checkFileExistsAndReadable(sdkBundlePath); + FilePreconditions.checkFileHasExtension("ASB file", sdkBundlePath, ".asb"); + } + + private static void validateSdkArchivePath(Path sdkArchivePath) { + FilePreconditions.checkFileExistsAndReadable(sdkArchivePath); + FilePreconditions.checkFileHasExtension("ASAR file", sdkArchivePath, ".asar"); + } + public static CommandHelp help() { return CommandHelp.builder() .setCommandName(COMMAND_NAME) @@ -355,7 +422,17 @@ public static CommandHelp help() { FlagDescription.builder() .setFlagName(SDK_BUNDLE_LOCATION_FLAG.getName()) .setExampleValue("path/to/SDKbundle.asb") - .setDescription("Path to SDK bundle. Must have the extension '.asb'.") + .setDescription( + "Path to SDK bundle. Must have the extension '.asb'. Cannot be used together" + + " with the `sdk-archive` flag.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(SDK_ARCHIVE_LOCATION_FLAG.getName()) + .setExampleValue("path/to/sdk.asar") + .setDescription( + "Path to SDK archive. Must have the extension '.asar'. Cannot be used together" + + " with the `sdk-bundle` flag.") .build()) .addFlag( FlagDescription.builder() diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java index c66a3aed..88d2de1d 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java @@ -91,7 +91,7 @@ public abstract class InstallMultiApksCommand { public static final ImmutableMap NONUPDATABLE_PACKAGES_PAIRS = ImmutableMap.of( - "com.google.android.ext.service", "com.google.android.extservice", + "com.google.android.ext.services", "com.google.android.extservices", "com.google.android.permissioncontroller", "com.google.android.permission"); private static final Flag ADB_PATH_FLAG = Flag.path("adb"); diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/BundleModuleMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/BundleModuleMerger.java index ab578279..a2512ed9 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/BundleModuleMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/BundleModuleMerger.java @@ -21,13 +21,13 @@ import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.FUSE_APPLICATION_ELEMENTS_FROM_FEATURE_MANIFESTS; import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.MERGE_INSTALL_TIME_MODULES_INTO_BASE; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESPECT_LEGACY_ON_DEMAND_ATTRIBUTE_FOR_INSTALL_MODULES_MERGING; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Multimaps.toMultimap; import com.android.aapt.Resources.ResourceTable; -import com.android.bundle.Config.BundleConfig; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -65,10 +65,7 @@ public static AppBundle mergeNonRemovableInstallTimeModules( Stream.concat( Stream.of(appBundle.getBaseModule()), appBundle.getFeatureModules().values().stream() - .filter( - module -> - shouldMerge( - module, appBundle.getBundleConfig(), overrideBundleToolVersion))) + .filter(module -> shouldMerge(module, appBundle, overrideBundleToolVersion))) .collect(toImmutableSet()); // If only base module should be fused there is nothing to do. @@ -110,27 +107,28 @@ public static AppBundle mergeNonRemovableInstallTimeModules( } private static boolean shouldMerge( - BundleModule module, BundleConfig bundleConfig, boolean overrideBundleToolVersion) { + BundleModule module, AppBundle appBundle, boolean overrideBundleToolVersion) { if (module.getModuleType() != ModuleType.FEATURE_MODULE) { return false; } + // Modules in AABs with isolated splits should never be merged. + if (appBundle.getBaseModule().getAndroidManifest().getIsolatedSplits().orElse(false)) { + return false; + } - return module - .getAndroidManifest() - .getManifestDeliveryElement() - .map( - manifestDeliveryElement -> { - Version bundleToolVersion = - BundleToolVersion.getVersionFromBundleConfig(bundleConfig); - // Only override for bundletool version < 1.0.0 - if (overrideBundleToolVersion - && !MERGE_INSTALL_TIME_MODULES_INTO_BASE.enabledForVersion(bundleToolVersion)) { - return manifestDeliveryElement.hasInstallTimeElement() - && !manifestDeliveryElement.hasModuleConditions(); - } - return !manifestDeliveryElement.isInstallTimeRemovable(bundleToolVersion); - }) - .orElse(false); + Version bundleToolVersion = + BundleToolVersion.getVersionFromBundleConfig(appBundle.getBundleConfig()); + if (!overrideBundleToolVersion + && !MERGE_INSTALL_TIME_MODULES_INTO_BASE.enabledForVersion(bundleToolVersion)) { + return false; + } + + boolean isAlwaysInstalledModule = module.getAndroidManifest().isAlwaysInstalledModule(); + return RESPECT_LEGACY_ON_DEMAND_ATTRIBUTE_FOR_INSTALL_MODULES_MERGING.enabledForVersion( + bundleToolVersion) + ? isAlwaysInstalledModule + : isAlwaysInstalledModule + && module.getAndroidManifest().getManifestDeliveryElement().isPresent(); } private static ImmutableSet getAllEntriesExceptDexAndSpecial( diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/FusingAndroidManifestMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/FusingAndroidManifestMerger.java index 5dc26c45..f547eb0c 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/FusingAndroidManifestMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/FusingAndroidManifestMerger.java @@ -293,7 +293,7 @@ private static String getNameAttribute(XmlProtoElement element) { private static ImmutableMap ensureOneManifestPerModule( SetMultimap manifests) { ImmutableMap.Builder builder = ImmutableMap.builder(); - for (BundleModuleName moduleName : manifests.keys()) { + for (BundleModuleName moduleName : manifests.keySet()) { Set moduleManifests = manifests.get(moduleName); if (moduleManifests.size() != 1) { throw CommandExecutionException.builder() diff --git a/src/main/java/com/android/tools/build/bundletool/model/AbiName.java b/src/main/java/com/android/tools/build/bundletool/model/AbiName.java index 40d9fca4..fd429a17 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AbiName.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AbiName.java @@ -39,7 +39,8 @@ public enum AbiName { X86("x86", 32), X86_64("x86_64", 64), MIPS("mips", 32), - MIPS64("mips64", 64); + MIPS64("mips64", 64), + RISCV64("riscv64", 64); private final String platformName; 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 cd343ccb..ecdcee41 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -19,7 +19,7 @@ import static com.android.tools.build.bundletool.model.ModuleDeliveryType.ALWAYS_INITIAL_INSTALL; import static com.android.tools.build.bundletool.model.ModuleDeliveryType.CONDITIONAL_INITIAL_INSTALL; import static com.android.tools.build.bundletool.model.ModuleDeliveryType.NO_INITIAL_INSTALL; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; +import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_U_API_VERSION; 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; @@ -142,6 +142,8 @@ public abstract class AndroidManifest { public static final String PATH_PATTERN_NAME = "pathPattern"; public static final String SCHEME_NAME = "scheme"; public static final String HOST_NAME = "host"; + public static final String SPLIT_TYPES_ATTRIBUTE_NAME = "splitTypes"; + public static final String REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME = "requiredSplitTypes"; public static final String SDK_PATCH_VERSION_ATTRIBUTE_NAME = "com.android.vending.sdk.version.patch"; @@ -156,7 +158,7 @@ public abstract class AndroidManifest { "com.google.wear.watchface.format.version"; public static final String REQUIRED_ATTRIBUTE_NAME = "required"; - public static final int SDK_SANDBOX_MIN_VERSION = ANDROID_T_API_VERSION; + public static final int SDK_SANDBOX_MIN_VERSION = ANDROID_U_API_VERSION; public static final String USES_SDK_LIBRARY_ELEMENT_NAME = "uses-sdk-library"; public static final String CERTIFICATE_DIGEST_ATTRIBUTE_NAME = "certDigest"; public static final String TARGET_SANDBOX_VERSION_ATTRIBUTE_NAME = "targetSandboxVersion"; @@ -242,6 +244,8 @@ public abstract class AndroidManifest { public static final int PATH_PREFIX_RESOURCE_ID = 0x0101002b; public static final int SCHEME_RESOURCE_ID = 0x01010027; public static final int HOST_RESOURCE_ID = 0x01010028; + public static final int SPLIT_TYPES_RESOURCE_ID = 0x0101064f; + public static final int REQUIRED_SPLIT_TYPES_RESOURCE_ID = 0x0101064e; // Matches the value of android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, used when turning // a manifest attribute which references a prerelease API version (e.g., "Q") into an integer. @@ -639,6 +643,39 @@ public boolean isDeliveryTypeDeclared() { return getOnDemandAttribute().isPresent(); } + /** + * Returns whether this module will be present in all app installations. + * + *

This does not differentiate between fused and unfused modules. Fused modules are considered + * to be "installed" even though that is as part of another module. + */ + public boolean isAlwaysInstalledModule() { + boolean hasDeliveryDeclaration = + getManifestDeliveryElement().isPresent() + && getManifestDeliveryElement().get().isWellFormed(); + boolean isLegacyOnDemand = + getOnDemandAttribute().map(XmlProtoAttribute::getValueAsBoolean).orElse(false); + + if (!hasDeliveryDeclaration) { + // The presence of a legacy on-demand attribute to fall back to (with no delivery element) + // implies that this module is not always present. + return !isLegacyOnDemand; + } + + // If the module is removable or conditional then it's not always installed. + ManifestDeliveryElement manifestDeliveryElement = getManifestDeliveryElement().get(); + if (manifestDeliveryElement.hasOnDemandElement() + || manifestDeliveryElement.hasFastFollowElement() + || manifestDeliveryElement.hasModuleConditions() + || manifestDeliveryElement.getInstallTimeRemovableValue().equals(Optional.of(true))) { + return false; + } + + // Else, this module has no conditions, it's always present. + // Important! This module may be fused into the base module later. + return true; + } + public Optional isInstantModule() { if (getInstantManifestDeliveryElement().isPresent()) { if (!getModuleType().equals(ModuleType.ASSET_MODULE)) { @@ -668,6 +705,33 @@ public Optional getExtractNativeLibsValue() { return getApplicationAttributeAsBoolean(EXTRACT_NATIVE_LIBS_RESOURCE_ID); } + /** + * Extracts the 'android:isSplitRequired' value from the {@code } tag. + * + *

Warning: this value is not read by the system and is provided for legacy install verifiers + * only. + * + * @return An optional containing the value of the 'isSplitRequired' attribute if set, or an empty + * optional if not set. + */ + public Optional getSplitsRequiredValue() { + return getApplicationAttributeAsBoolean(IS_SPLIT_REQUIRED_RESOURCE_ID); + } + + /** Extracts the 'android:splitTypes' value from the {@code } tag. */ + public Optional> getProvidedSplitTypesValue() { + return getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .map(value -> ImmutableList.copyOf(COMMA_SPLITTER.split(value.getValueAsString()))); + } + + /** Extracts the 'android:requiredSplitTypes' value from the {@code } tag. */ + public Optional> getRequiredSplitTypesValue() { + return getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .map(value -> ImmutableList.copyOf(COMMA_SPLITTER.split(value.getValueAsString()))); + } + /** Returns the string value of the 'installLocation' attribute if set. */ public Optional getInstallLocationValue() { return getManifestElement() 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 a766e275..d3469c1f 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -106,14 +106,6 @@ public static AppBundle buildFromModules( ImmutableList modules, BundleConfig bundleConfig, BundleMetadata bundleMetadata) { - ImmutableSet pinnedResourceIds = - bundleConfig.getMasterResources().getResourceIdsList().stream() - .map(ResourceId::create) - .collect(toImmutableSet()); - - ImmutableSet pinnedResourceNames = - ImmutableSet.copyOf(bundleConfig.getMasterResources().getResourceNamesList()); - ImmutableListMultimap runtimeEnabledSdkDependencies = modules.stream() .filter(module -> module.getRuntimeEnabledSdkConfig().isPresent()) @@ -126,8 +118,6 @@ public static AppBundle buildFromModules( return builder() .setModules(Maps.uniqueIndex(modules, BundleModule::getName)) - .setMasterPinnedResourceIds(pinnedResourceIds) - .setMasterPinnedResourceNames(pinnedResourceNames) .setBundleConfig(bundleConfig) .setBundleMetadata(bundleMetadata) .setRuntimeEnabledSdkDependencies( @@ -139,17 +129,6 @@ public static AppBundle buildFromModules( @Override public abstract ImmutableMap getModules(); - /** - * Resource IDs that must remain in the master split regardless of their targeting configuration. - */ - public abstract ImmutableSet getMasterPinnedResourceIds(); - - /** - * Resource names that must remain in the master split regardless of their targeting - * configuration. - */ - public abstract ImmutableSet getMasterPinnedResourceNames(); - public abstract BundleConfig getBundleConfig(); @Override @@ -326,10 +305,6 @@ public Builder addRawModule(BundleModule bundleModule) { return this; } - public abstract Builder setMasterPinnedResourceIds(ImmutableSet pinnedResourceIds); - - public abstract Builder setMasterPinnedResourceNames(ImmutableSet pinnedResourceNames); - public abstract Builder setBundleConfig(BundleConfig bundleConfig); public abstract Builder setBundleMetadata(BundleMetadata bundleMetadata); diff --git a/src/main/java/com/android/tools/build/bundletool/model/BinaryArtProfileConstants.java b/src/main/java/com/android/tools/build/bundletool/model/BinaryArtProfileConstants.java new file mode 100644 index 00000000..acb9143b --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/BinaryArtProfileConstants.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 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; + +/** Constants related to baseline profiles. */ +public final class BinaryArtProfileConstants { + + /** + * Location for baseline profiles in an APK. + * + *

This is the location where Jetpack Profile Installer will fetch them from, to install + * baseline profiles on devices where dex metadata files are missing at install-time. + */ + public static final String PROFILE_APK_LOCATION = "assets/dexopt"; + + /** + * Subfolder of the bundle's {@code BUNDLE-METADATA} folder where baseline profiles are located. + */ + public static final String PROFILE_METADATA_NAMESPACE = "com.android.tools.build.profiles"; + + /** + * Legacy subfolder of the bundle's {@code BUNDLE-METADATA} folder where baseline profiles used to + * be located. + */ + public static final String LEGACY_PROFILE_METADATA_NAMESPACE = "assets.dexopt"; + + /** Name of the baseline profile file. */ + public static final String PROFILE_FILENAME = "baseline.prof"; + + /** Name of the baseline profile metadata file. */ + public static final String PROFILE_METADATA_FILENAME = "baseline.profm"; + + private BinaryArtProfileConstants() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/BundleMetadata.java b/src/main/java/com/android/tools/build/bundletool/model/BundleMetadata.java index 0c7be2e1..565ac9dd 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/BundleMetadata.java +++ b/src/main/java/com/android/tools/build/bundletool/model/BundleMetadata.java @@ -53,7 +53,13 @@ public abstract class BundleMetadata { */ public abstract ImmutableMap getFileContentMap(); - public abstract Builder toBuilder(); + public Builder toBuilder() { + Builder builder = autoToBuilder(); + builder.fileContentMapBuilder().putAll(getFileContentMap()); + return builder; + } + + abstract Builder autoToBuilder(); public static Builder builder() { return new AutoValue_BundleMetadata.Builder(); @@ -102,7 +108,8 @@ private static ZipPath checkMetadataPath(ZipPath path) { @AutoValue.Builder public abstract static class Builder { - abstract ImmutableMap.Builder fileContentMapBuilder(); + private final ImmutableMap.Builder fileContentMapBuilder = + ImmutableMap.builder(); /** Adds metadata file {@code /}. */ public Builder addFile(String namespacedDir, String fileName, ByteSource content) { @@ -115,10 +122,29 @@ public Builder addFile(String namespacedDir, String fileName, ByteSource content * @param path path of the file inside the bundle metadata directory */ public Builder addFile(ZipPath path, ByteSource content) { - fileContentMapBuilder().put(checkMetadataPath(path), content); + fileContentMapBuilder.put(checkMetadataPath(path), content); return this; } - public abstract BundleMetadata build(); + /** Returns a builder for the file content map. */ + public ImmutableMap.Builder fileContentMapBuilder() { + return fileContentMapBuilder; + } + + abstract Builder setFileContentMap(ImmutableMap fileContentMap); + + abstract BundleMetadata autoBuild(); + + /** Build the file content map, throwing an exception if any key was added more than once. */ + public BundleMetadata build() { + setFileContentMap(fileContentMapBuilder.buildOrThrow()); + return autoBuild(); + } + + /** Build the file content map, keeping the last entry if any key was added more than once. */ + public BundleMetadata buildKeepingLast() { + setFileContentMap(fileContentMapBuilder.buildKeepingLast()); + return autoBuild(); + } } } 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 aaec5e0c..4619f574 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java @@ -29,7 +29,6 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingByDeterministic; -import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.MERGE_INSTALL_TIME_MODULES_INTO_BASE; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.counting; @@ -110,14 +109,6 @@ public boolean hasInstallTimeElement() { .isPresent(); } - public boolean isInstallTimeRemovable(Version bundleToolVersion) { - if (hasOnDemandElement() || hasFastFollowElement() || hasModuleConditions()) { - return true; - } - return getInstallTimeRemovableValue() - .orElse(!MERGE_INSTALL_TIME_MODULES_INTO_BASE.enabledForVersion(bundleToolVersion)); - } - /** * Returns "removable" attribute value of "install-time" element. * 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 161a1624..3f200863 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -59,6 +59,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_BY_PRIVACY_SANDBOX_SDK_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RESOURCE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.ROUND_ICON_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.ROUND_ICON_RESOURCE_ID; @@ -68,6 +69,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_VERSION_MAJOR_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_ATTRIBUTE_NAME; @@ -292,6 +294,9 @@ public ManifestEditor setFusedModuleNames(ImmutableList moduleNames) { /** * Sets a flag whether the app is able to run without any config splits. * + *

Warning: this value is not read by the system and is provided for legacy install verifiers + * only. + * *

The information is stored as: * *

    @@ -310,6 +315,40 @@ public ManifestEditor setSplitsRequired(boolean value) { IS_SPLIT_REQUIRED_ATTRIBUTE_NAME, IS_SPLIT_REQUIRED_RESOURCE_ID, value); } + /** + * Sets the list of split types provided by this split. + * + *

    Split types are arbitrary strings, stored comma-separated, and used along with {@code + * requiredSplitTypes} to perform validation at install time. + * + *

    Note: this is currently set under `dist:splitTypes` rather than as the Android attribute. + * This will be replaced once the verifier implementation has been validated. + */ + @CanIgnoreReturnValue + public ManifestEditor setSplitTypes(ImmutableList splitTypes) { + manifestElement + .getOrCreateAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); + return this; + } + + /** + * Sets the list of required split types for this split. + * + *

    Split types are arbitrary strings, stored comma-separated, and reference the split types + * provided by {@code splitTypes} in splits. + * + *

    Note: this is currently set under `dist:requiredSplitTypes` rather than as the Android + * attribute. This will be replaced once the verifier implementation has been validated. + */ + @CanIgnoreReturnValue + public ManifestEditor setRequiredSplitTypes(ImmutableList splitTypes) { + manifestElement + .getOrCreateAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); + return this; + } + /** Adds an empty {@code } element in the manifest if none is present. */ @CanIgnoreReturnValue public ManifestEditor addApplicationElementIfMissing() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestMutator.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestMutator.java index d76501dc..58328de4 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestMutator.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestMutator.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import java.util.function.Consumer; @@ -23,11 +24,23 @@ @Immutable public interface ManifestMutator extends Consumer { + /** Add the {@code extractNativeLibs} attribute to the manifest. */ static ManifestMutator withExtractNativeLibs(boolean value) { return manifestEditor -> manifestEditor.setExtractNativeLibsValue(value); } + /** Add the {@code isSplitRequired} attribute to the manifest. */ static ManifestMutator withSplitsRequired(boolean value) { return manifestEditor -> manifestEditor.setSplitsRequired(value); } + + /** Add the {@code splitTypes} attribute to a manifest. */ + static ManifestMutator withProvidedSplitTypes(ImmutableList splitTypes) { + return manifestEditor -> manifestEditor.setSplitTypes(splitTypes); + } + + /** Add the {@code requiredSplitTypes} attribute to a manifest. */ + static ManifestMutator withRequiredSplitTypes(ImmutableList splitTypes) { + return manifestEditor -> manifestEditor.setRequiredSplitTypes(splitTypes); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java b/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java new file mode 100644 index 00000000..c90fa6e3 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.bundle.Targeting.ApkTargeting; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; + +/** Injector for required and provided split types. */ +public class RequiredSplitTypesInjector { + + /** + * Injects required and provided split types into the provided module split. + * + * @return the modified {@link ModuleSplit} + */ + @CheckReturnValue + public static ImmutableList injectSplitTypeValidation( + ImmutableList splits, ImmutableList requiredModules) { + return splits.stream() + .map( + split -> { + ManifestEditor apkManifest = split.getAndroidManifest().toEditor(); + + apkManifest.setSplitTypes( + getProvidedSplitTypes(split).stream() + .map(RequiredSplitTypeName::toAttributeValue) + .collect(toImmutableList())); + + // Only base/feature modules have required split types. + if (split.isMasterSplit()) { + apkManifest.setRequiredSplitTypes( + getRequiredSplitTypes(splits, requiredModules, split).stream() + .map(RequiredSplitTypeName::toAttributeValue) + .collect(toImmutableList())); + } + + return split.toBuilder().setAndroidManifest(apkManifest.save()).build(); + }) + .collect(toImmutableList()); + } + + private static ImmutableSet getProvidedSplitTypes( + ModuleSplit moduleSplit) { + ApkTargeting apkTargeting = moduleSplit.getApkTargeting(); + BundleModuleName moduleName = moduleSplit.getModuleName(); + ImmutableSet.Builder splitTypes = ImmutableSet.builder(); + + if (apkTargeting.hasAbiTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.ABI)); + } + if (apkTargeting.hasScreenDensityTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.DENSITY)); + } + if (apkTargeting.hasTextureCompressionFormatTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.TEXTURE_FORMAT)); + } + if (apkTargeting.hasDeviceTierTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.DEVICE_TIER)); + } + if (apkTargeting.hasCountrySetTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.COUNTRY_SET)); + } + + if (moduleSplit.isMasterSplit() && !moduleSplit.isBaseModuleSplit()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.MODULE)); + } + + return splitTypes.build(); + } + + private static ImmutableSet getRequiredSplitTypes( + ImmutableList allSplits, + ImmutableList requiredModules, + ModuleSplit moduleSplit) { + BundleModuleName moduleName = moduleSplit.getModuleName(); + ImmutableSet.Builder splitTypes = ImmutableSet.builder(); + + for (ModuleSplit split : allSplits) { + if (!split.getModuleName().equals(moduleName)) { + continue; + } + + ApkTargeting apkTargeting = split.getApkTargeting(); + + if (apkTargeting.hasAbiTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.ABI)); + } + if (apkTargeting.hasScreenDensityTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.DENSITY)); + } + if (apkTargeting.hasTextureCompressionFormatTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.TEXTURE_FORMAT)); + } + if (apkTargeting.hasDeviceTierTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.DEVICE_TIER)); + } + if (apkTargeting.hasCountrySetTargeting()) { + splitTypes.add(RequiredSplitTypeName.create(moduleName, RequiredSplitType.COUNTRY_SET)); + } + } + + if (moduleSplit.isBaseModuleSplit()) { + requiredModules.stream() + .filter(requiredModuleName -> !requiredModuleName.equals(moduleName)) + .forEach( + requiredModuleName -> + splitTypes.add( + RequiredSplitTypeName.create(requiredModuleName, RequiredSplitType.MODULE))); + } + + return splitTypes.build(); + } + + private RequiredSplitTypesInjector() {} + + static enum RequiredSplitType { + ABI("abi"), + DENSITY("density"), + TEXTURE_FORMAT("textures"), + DEVICE_TIER("tier"), + COUNTRY_SET("countries"), + MODULE("module"); + + private final String label; + + private RequiredSplitType(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + } + + @AutoValue + abstract static class RequiredSplitTypeName { + abstract BundleModuleName getModuleName(); + + abstract RequiredSplitType getSplitType(); + + public static RequiredSplitTypeName create( + BundleModuleName moduleName, RequiredSplitType splitType) { + return new AutoValue_RequiredSplitTypesInjector_RequiredSplitTypeName(moduleName, splitType); + } + + public String toAttributeValue() { + return String.format("%s__%s", getModuleName().getName(), getSplitType().getLabel()); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java b/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java index d8270195..1195a025 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model; import static com.android.tools.build.bundletool.model.utils.BundleParser.extractModules; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readSdkInterfaceDescriptors; import static com.android.tools.build.bundletool.model.utils.BundleParser.readSdkModulesConfig; import static com.android.tools.build.bundletool.model.utils.BundleParser.sanitize; @@ -66,6 +67,7 @@ public static SdkAsar buildFromZip(ZipFile asar, ZipFile modulesFile, Path modul .setSdkModulesConfig(sdkModulesConfig) .setModulesFile(modulesFilePath.toFile()) .setSdkMetadata(readSdkMetadata(asar)); + readSdkInterfaceDescriptors(asar).ifPresent(sdkAsarBuilder::setSdkInterfaceDescriptors); Document document; try { document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java b/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java index 4497e906..622666fb 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkBundle.java @@ -70,6 +70,20 @@ public static SdkBundle buildFromZip( return bundle.build(); } + /** Builds an {@link SdkBundle} from the given {@link SdkAsar}. */ + public static SdkBundle buildFromAsar(SdkAsar sdkAsar, Integer versionCode) { + SdkBundle.Builder sdkBundleBuilder = + SdkBundle.builder() + .setModule(sdkAsar.getModule()) + .setSdkModulesConfig(sdkAsar.getSdkModulesConfig()) + .setVersionCode(versionCode) + // ASAR format does not contain SdkBundleConfig or BundleMetadata. + .setSdkBundleConfig(SdkBundleConfig.getDefaultInstance()) + .setBundleMetadata(BundleMetadata.builder().build()); + sdkAsar.getSdkInterfaceDescriptors().ifPresent(sdkBundleBuilder::setSdkInterfaceDescriptors); + return sdkBundleBuilder.build(); + } + public abstract BundleModule getModule(); @Override diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingComparators.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingComparators.java index 6a8d38b8..f8d82a1c 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingComparators.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingComparators.java @@ -47,7 +47,7 @@ public final class TargetingComparators { * This should verify the following statements in order: * *

      - *
    • arm < x86 < mips + *
    • arm < x86 < mips < riscv *
    • 32 bits < 64 bits *
    • less recent version of CPU < more recent version of CPU */ @@ -59,7 +59,8 @@ public final class TargetingComparators { AbiAlias.X86, AbiAlias.X86_64, AbiAlias.MIPS, - AbiAlias.MIPS64); + AbiAlias.MIPS64, + AbiAlias.RISCV64); /** The order of preference for textures, in case multiple formats are available. */ public static final Ordering TEXTURE_COMPRESSION_FORMAT_ORDERING = diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/Versions.java b/src/main/java/com/android/tools/build/bundletool/model/utils/Versions.java index 49a28ddc..3df4fe5a 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/Versions.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/Versions.java @@ -30,6 +30,7 @@ public final class Versions { public static final int ANDROID_S_API_VERSION = 31; public static final int ANDROID_S_V2_API_VERSION = 32; public static final int ANDROID_T_API_VERSION = 33; + public static final int ANDROID_U_API_VERSION = 34; // Not meant to be instantiated. private Versions() {} 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 cdb4860b..9abf9e71 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "1.15.4"; + private static final String CURRENT_VERSION = "1.15.5"; /** Returns the version of BundleTool being run. */ 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 0d7ce238..fb94e9a7 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java @@ -105,7 +105,14 @@ public enum VersionGuardedFeature { *

      Now, features with no density specific resources (or with a single density specific * resource) will not have density splits generated for them. */ - FIX_SKIP_GENERATING_EMPTY_DENSITY_SPLITS("1.15.1"); + FIX_SKIP_GENERATING_EMPTY_DENSITY_SPLITS("1.15.1"), + + /** + * Respect legacy onDemand attribute for install time module merging. + * + *

      Previously onDemand=false would not be respected and modules may not have been merged. + */ + RESPECT_LEGACY_ON_DEMAND_ATTRIBUTE_FOR_INSTALL_MODULES_MERGING("1.15.4"); /** Version from which the given feature should be enabled by default. */ private final Version enabledSinceVersion; diff --git a/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java b/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java index 67923ebd..90a34ee1 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java @@ -28,6 +28,7 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.ResourceTableEntry; import com.android.tools.build.bundletool.model.version.Version; import com.android.tools.build.bundletool.splitters.AbiApexImagesSplitter; import com.android.tools.build.bundletool.splitters.AbiNativeLibrariesSplitter; @@ -41,6 +42,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.util.Optional; +import java.util.function.Predicate; import javax.inject.Inject; /** Splits bundle modules into module splits that are next merged into standalone APKs. */ @@ -158,7 +160,17 @@ private SplittingPipeline createResourcesSplittingPipeline( } if (shardingDimensions.contains(OptimizationDimension.LANGUAGE) && shouldSplitByLanguage()) { - resourceSplitters.add(new LanguageResourcesSplitter()); + ImmutableSet pinnedResourceIds = + ImmutableSet.copyOf(bundleConfig.getMasterResources().getResourceIdsList()); + ImmutableSet pinnedResourceNames = + ImmutableSet.copyOf(bundleConfig.getMasterResources().getResourceNamesList()); + + Predicate pinLangResourceToMaster = + Predicates.or( + // Resources that are unconditionally in the master split. + entry -> pinnedResourceIds.contains(entry.getResourceId().getFullResourceId()), + entry -> pinnedResourceNames.contains(entry.getEntry().getName())); + resourceSplitters.add(new LanguageResourcesSplitter(pinLangResourceToMaster)); } return new SplittingPipeline(resourceSplitters.build()); diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java index 5233a10a..e98ef5c2 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java @@ -80,6 +80,8 @@ public boolean shouldStripTargetingSuffix(OptimizationDimension dimension) { */ public abstract Optional getMinSdkForAdditionalVariantWithV3Rotation(); + public abstract boolean getEnableRequiredSplitTypes(); + public abstract Builder toBuilder(); public static Builder builder() { @@ -96,7 +98,8 @@ public static Builder builder() { .setMasterPinnedResourceIds(ImmutableSet.of()) .setMasterPinnedResourceNames(ImmutableSet.of()) .setBaseManifestReachableResources(ImmutableSet.of()) - .setSuffixStrippings(ImmutableMap.of()); + .setSuffixStrippings(ImmutableMap.of()) + .setEnableRequiredSplitTypes(false); } public static ApkGenerationConfiguration getDefaultInstance() { @@ -141,6 +144,8 @@ public abstract Builder setMinSdkForAdditionalVariantWithV3Rotation( public abstract Builder setEnableBaseModuleMinSdkAsDefaultTargeting( boolean enableBaseModuleMinSdkAsDefaultTargeting); + public abstract Builder setEnableRequiredSplitTypes(boolean enableRequiredSplitTypes); + public abstract ApkGenerationConfiguration build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java b/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java index 0266dd41..61b70d51 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java @@ -15,6 +15,12 @@ */ package com.android.tools.build.bundletool.splitters; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.LEGACY_PROFILE_METADATA_NAMESPACE; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_APK_LOCATION; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_FILENAME; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_METADATA_FILENAME; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_METADATA_NAMESPACE; + import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -27,20 +33,12 @@ /** Copies the binary art profiles from AAB's metadata into main APK of base module. */ public final class BinaryArtProfilesInjector { - static final String APK_LOCATION = "assets/dexopt"; - static final String METADATA_NAMESPACE = "com.android.tools.build.profiles"; - static final String LEGACY_METADATA_NAMESPACE = "assets.dexopt"; - - static final String BINARY_ART_PROFILE_NAME = "baseline.prof"; - static final String BINARY_ART_PROFILE_METADATA_NAME = "baseline.profm"; - private final Optional binaryArtProfile; private final Optional binaryArtProfileMetadata; public BinaryArtProfilesInjector(AppBundle appBundle) { - binaryArtProfile = extract(appBundle.getBundleMetadata(), BINARY_ART_PROFILE_NAME); - binaryArtProfileMetadata = - extract(appBundle.getBundleMetadata(), BINARY_ART_PROFILE_METADATA_NAME); + binaryArtProfile = extract(appBundle.getBundleMetadata(), PROFILE_FILENAME); + binaryArtProfileMetadata = extract(appBundle.getBundleMetadata(), PROFILE_METADATA_FILENAME); } public ModuleSplit inject(ModuleSplit split) { @@ -58,7 +56,7 @@ public ModuleSplit inject(ModuleSplit split) { ModuleEntry.builder() .setForceUncompressed(true) .setContent(content) - .setPath(ZipPath.create(APK_LOCATION).resolve(BINARY_ART_PROFILE_NAME)) + .setPath(ZipPath.create(PROFILE_APK_LOCATION).resolve(PROFILE_FILENAME)) .build())); binaryArtProfileMetadata.ifPresent( content -> @@ -66,7 +64,8 @@ public ModuleSplit inject(ModuleSplit split) { ModuleEntry.builder() .setForceUncompressed(true) .setContent(content) - .setPath(ZipPath.create(APK_LOCATION).resolve(BINARY_ART_PROFILE_METADATA_NAME)) + .setPath( + ZipPath.create(PROFILE_APK_LOCATION).resolve(PROFILE_METADATA_FILENAME)) .build())); return builder.build(); } @@ -77,9 +76,10 @@ private static boolean shouldInjectBinaryArtProfile(ModuleSplit split) { } private static Optional extract(BundleMetadata metadata, String entryName) { - Optional entry = metadata.getFileAsByteSource(METADATA_NAMESPACE, entryName); + Optional entry = + metadata.getFileAsByteSource(PROFILE_METADATA_NAMESPACE, entryName); return entry.isPresent() ? entry - : metadata.getFileAsByteSource(LEGACY_METADATA_NAMESPACE, entryName); + : metadata.getFileAsByteSource(LEGACY_PROFILE_METADATA_NAMESPACE, entryName); } } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitter.java index df126de2..ed666e42 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitter.java @@ -17,7 +17,7 @@ package com.android.tools.build.bundletool.splitters; import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; +import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_P_API_VERSION; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -47,8 +47,8 @@ public ImmutableCollection split(ModuleSplit moduleSplit) { return ImmutableList.of(moduleSplit); } - // Only APKs targeting devices below Android Q should have dex entries compressed. - boolean forceUncompressed = targetsAtLeastQ(moduleSplit); + // Only APKs targeting devices below Android P should have dex entries compressed. + boolean forceUncompressed = targetsAtLeastP(moduleSplit); return ImmutableList.of( createModuleSplit( moduleSplit, mergeAndSetCompression(dexEntries, moduleSplit, forceUncompressed))); @@ -73,14 +73,14 @@ private static ImmutableList mergeAndSetCompression( .build(); } - private static boolean targetsAtLeastQ(ModuleSplit moduleSplit) { + private static boolean targetsAtLeastP(ModuleSplit moduleSplit) { int sdkVersion = Iterables.getOnlyElement( moduleSplit.getVariantTargeting().getSdkVersionTargeting().getValueList()) .getMin() .getValue(); - return sdkVersion >= ANDROID_Q_API_VERSION; + return sdkVersion >= ANDROID_P_API_VERSION; } private static ModuleSplit createModuleSplit( diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionVariantGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionVariantGenerator.java index 72e7d5e7..2ba5678f 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionVariantGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/DexCompressionVariantGenerator.java @@ -20,11 +20,11 @@ import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionTargeting; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.variantTargeting; +import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_P_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_S_API_VERSION; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import com.android.bundle.Config.UncompressDexFiles.UncompressedDexTargetSdk; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -55,17 +55,17 @@ public Stream generate(BundleModule module) { return Stream.of(); } Stream.Builder variantTargetings = Stream.builder(); - if (apkGenerationConfiguration - .getDexCompressionSplitterForTargetSdk() - .equals(UncompressedDexTargetSdk.SDK_31)) { - variantTargetings.add( - variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_S_API_VERSION)))); - } else { - variantTargetings.add( - variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_Q_API_VERSION)))); + switch (apkGenerationConfiguration.getDexCompressionSplitterForTargetSdk()) { + case SDK_31: + variantTargetings.add( + variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_S_API_VERSION)))); + break; + default: + // Uncompressed dex are supported starting from Android P, but only starting from Android Q + // the performance impact is negligible compared to a compressed dex. + variantTargetings.add( + variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_Q_API_VERSION)))); } - // Uncompressed dex are supported starting from Android P, but only starting from Android Q the - // performance impact is negligible compared to a compressed dex. return variantTargetings.build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/LanguageResourcesSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/LanguageResourcesSplitter.java index 1d3d2790..86575200 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/LanguageResourcesSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/LanguageResourcesSplitter.java @@ -56,10 +56,6 @@ public class LanguageResourcesSplitter extends SplitterForOneTargetingDimension private final Predicate pinResourceToMaster; - public LanguageResourcesSplitter() { - this(/* pinResourceToMaster= */ Predicates.alwaysFalse()); - } - public LanguageResourcesSplitter(Predicate pinResourceToMaster) { this.pinResourceToMaster = pinResourceToMaster; } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java index 70b9115f..d12c914a 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java @@ -23,7 +23,9 @@ import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; +import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.RequiredSplitTypesInjector; import com.android.tools.build.bundletool.model.SourceStamp; import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.version.Version; @@ -87,6 +89,17 @@ private ImmutableList generateSplitApks( StampType.STAMP_TYPE_DISTRIBUTION_APK); splits.addAll(moduleSplitter.splitModule()); } + + if (commonApkGenerationConfiguration.getEnableRequiredSplitTypes()) { + ImmutableList nonRemovableModules = + modulesForVariant.stream() + .filter(module -> module.getAndroidManifest().isAlwaysInstalledModule()) + .map(BundleModule::getName) + .collect(toImmutableList()); + return RequiredSplitTypesInjector.injectSplitTypeValidation( + splits.build(), nonRemovableModules); + } + return splits.build(); } diff --git a/src/main/proto/targeting.proto b/src/main/proto/targeting.proto index 888eb01a..9a53c01c 100644 --- a/src/main/proto/targeting.proto +++ b/src/main/proto/targeting.proto @@ -102,6 +102,7 @@ message Abi { X86_64 = 5; MIPS = 6; MIPS64 = 7; + RISCV64 = 8; } AbiAlias alias = 1; } 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 bac35b0e..72fff809 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -1538,6 +1538,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_customStorePackage() throws assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test public void missingBundleFile_throws() throws Exception { Path bundlePath = tmpDir.resolve("bundle.aab"); 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 49e1fd2a..2a23a09e 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -19,6 +19,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.RISCV64; 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; @@ -2203,6 +2204,7 @@ public void buildApksCommand_splitApks_targetLPlus() throws Exception { "base", builder -> builder.addFile("dex/classes.dex").setManifest(androidManifest("com.test.app"))) + .setBundleConfig(BundleConfigBuilder.create().setUncompressDexFiles(false).build()) .build(); TestComponent.useTestModule( this, @@ -2213,7 +2215,7 @@ public void buildApksCommand_splitApks_targetLPlus() throws Exception { ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); - assertThat(splitApkVariants(result)).hasSize(2); + assertThat(splitApkVariants(result)).hasSize(1); ImmutableList variants = splitApkVariants(result); variants.forEach(variant -> assertThat(variant.hasTargeting()).isTrue()); assertThat( @@ -2222,7 +2224,7 @@ public void buildApksCommand_splitApks_targetLPlus() throws Exception { variant -> variant.getTargeting().getSdkVersionTargeting().getValueList().stream()) .collect(toImmutableList())) - .containsExactly(L_SDK_VERSION, S_SDK_VERSION); + .containsExactly(L_SDK_VERSION); } @Test @@ -2238,7 +2240,7 @@ public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { .setNativeConfig( nativeLibraries( targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) - .setManifest(androidManifest("com.test.app", withMinSdkVersion(25)))) + .setManifest(androidManifest("com.test.app", withMinSdkVersion(31)))) .build(); TestComponent.useTestModule( this, @@ -2249,7 +2251,7 @@ public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); - assertThat(splitApkVariants(result)).hasSize(2); + assertThat(splitApkVariants(result)).hasSize(1); ImmutableList variants = splitApkVariants(result); variants.forEach(variant -> assertThat(variant.hasTargeting()).isTrue()); assertThat( @@ -2258,7 +2260,7 @@ public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { variant -> variant.getTargeting().getSdkVersionTargeting().getValueList().stream()) .collect(toImmutableList())) - .containsExactly(sdkVersionFrom(25), S_SDK_VERSION); + .containsExactly(sdkVersionFrom(31)); } @Test @@ -2695,7 +2697,7 @@ public void buildApksCommand_standalone_oneModuleOneVariant() throws Exception { } @Test - public void disabledUncompressedNativeLibraries_singleSplitVariant() throws Exception { + public void disabledUncompressedNativeLibrariesAndDex_singleSplitVariant() throws Exception { AppBundle appBundle = new AppBundleBuilder() .addModule( @@ -2709,7 +2711,10 @@ public void disabledUncompressedNativeLibraries_singleSplitVariant() throws Exce targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app"))) .setBundleConfig( - BundleConfigBuilder.create().setUncompressNativeLibraries(false).build()) + BundleConfigBuilder.create() + .setUncompressNativeLibraries(false) + .setUncompressDexFiles(false) + .build()) .build(); TestComponent.useTestModule( this, @@ -2722,14 +2727,12 @@ public void disabledUncompressedNativeLibraries_singleSplitVariant() throws Exce ImmutableList splitApkVariants = splitApkVariants(result); - assertThat(splitApkVariants).hasSize(2); + assertThat(splitApkVariants).hasSize(1); assertThat( splitApkVariants.stream() .map(variant -> variant.getTargeting().getSdkVersionTargeting()) .collect(toImmutableList())) - .containsExactly( - sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting(S_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); + .containsExactly(sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION))); } @Test @@ -2747,7 +2750,7 @@ public void defaultUncompressedLibraries_after_0_6_0_enabled_multipleSplitVarian nativeLibraries( targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app"))) - .setBundleConfig(BundleConfigBuilder.create().build()) + .setBundleConfig(BundleConfigBuilder.create().setUncompressDexFiles(false).build()) .build(); TestComponent.useTestModule( this, @@ -2763,12 +2766,8 @@ public void defaultUncompressedLibraries_after_0_6_0_enabled_multipleSplitVarian splitApkVariants.stream() .map(variant -> variant.getTargeting().getSdkVersionTargeting())) .containsExactly( - sdkVersionTargeting( - L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, M_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - M_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - S_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, M_SDK_VERSION))); + sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, M_SDK_VERSION)), + sdkVersionTargeting(M_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); } @Test @@ -2819,7 +2818,10 @@ public void enabledUncompressedNativeLibraries_nativeActivities_multipleSplitVar targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app", withNativeActivity("some")))) .setBundleConfig( - BundleConfigBuilder.create().setUncompressNativeLibraries(true).build()) + BundleConfigBuilder.create() + .setUncompressNativeLibraries(true) + .setUncompressDexFiles(false) + .build()) .build(); TestComponent.useTestModule( this, @@ -2835,12 +2837,8 @@ public void enabledUncompressedNativeLibraries_nativeActivities_multipleSplitVar splitApkVariants.stream() .map(variant -> variant.getTargeting().getSdkVersionTargeting())) .containsExactly( - sdkVersionTargeting( - L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, N_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - N_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - S_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, N_SDK_VERSION))); + sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, N_SDK_VERSION)), + sdkVersionTargeting(N_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); VariantProperties expectedVariantProperties = VariantProperties.newBuilder().setUncompressedNativeLibraries(true).build(); assertThat( @@ -2874,7 +2872,10 @@ public void enableNativeLibraryCompressionWithExternalStorage_multipleSplitVaria targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) .setManifest(androidManifest("com.test.app", withInstallLocation("auto")))) .setBundleConfig( - BundleConfigBuilder.create().setUncompressNativeLibraries(true).build()) + BundleConfigBuilder.create() + .setUncompressNativeLibraries(true) + .setUncompressDexFiles(false) + .build()) .build(); TestComponent.useTestModule( this, @@ -2890,12 +2891,8 @@ public void enableNativeLibraryCompressionWithExternalStorage_multipleSplitVaria splitApkVariants.stream() .map(variant -> variant.getTargeting().getSdkVersionTargeting())) .containsExactly( - sdkVersionTargeting( - L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, P_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - P_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting( - S_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION, P_SDK_VERSION))); + sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, P_SDK_VERSION)), + sdkVersionTargeting(P_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); } @Test @@ -3043,6 +3040,7 @@ public void enabledDexCompressionSplitter_disabledUncompressedDex_noUncompressed @Test public void dexCompressionIsNotSet_enabledByDefault() throws Exception { + SdkVersion expectedDefaultUncompressedDexSdk = S_SDK_VERSION; AppBundle appBundle = new AppBundleBuilder() .addModule( @@ -3065,19 +3063,18 @@ public void dexCompressionIsNotSet_enabledByDefault() throws Exception { splitApkVariants.stream() .map(variant -> variant.getTargeting().getSdkVersionTargeting())) .containsExactly( - sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, S_SDK_VERSION)), - sdkVersionTargeting(S_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); + sdkVersionTargeting( + L_SDK_VERSION, + ImmutableSet.of(LOWEST_SDK_VERSION, expectedDefaultUncompressedDexSdk)), + sdkVersionTargeting( + expectedDefaultUncompressedDexSdk, + ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION))); Variant uncompressedDexVariant = splitApkVariants.stream() - .filter( - variant -> - variant - .getTargeting() - .getSdkVersionTargeting() - .getValue(0) - .equals(S_SDK_VERSION)) + .filter(variant -> variant.getVariantProperties().getUncompressedDex()) .collect(onlyElement()); - assertThat(uncompressedDexVariant.getVariantProperties().getUncompressedDex()).isTrue(); + assertThat(uncompressedDexVariant.getTargeting().getSdkVersionTargeting().getValue(0)) + .isEqualTo(expectedDefaultUncompressedDexSdk); } @Test @@ -3320,6 +3317,12 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception targetedNativeDirectory( "lib/mips", nativeDirectoryTargeting(MIPS)))) .setManifest(androidManifest("com.test.app"))) + .setBundleConfig( + BundleConfigBuilder.create() + .setUncompressNativeLibraries(true) + .setUncompressDexFiles(true) + .setUncompressDexFilesForVariant(UncompressedDexTargetSdk.SDK_31) + .build()) .build(); TestComponent.useTestModule( this, @@ -3628,6 +3631,60 @@ public void buildApksCommand_system_withLanguageTargeting() throws Exception { .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); } + @Test + public void buildApksCommand_system_riscv() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .addFile("lib/riscv64/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(X86_64)), + targetedNativeDirectory( + "lib/riscv64", nativeDirectoryTargeting(RISCV64)))) + .setManifest(androidManifest("com.test.app"))) + .setBundleConfig(BundleConfigBuilder.create().addSplitDimension(Value.ABI).build()) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder() + .withAppBundle(appBundle) + .withOutputPath(outputFilePath) + .withApkBuildMode(SYSTEM) + .withDeviceSpec( + mergeSpecs( + sdkVersion(28), abis("riscv64"), density(DensityAlias.MDPI), locales("en-US"))) + .build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + assertThat(result.getVariantList()).hasSize(1); + assertThat(systemApkVariants(result)).hasSize(1); + + Variant systemVariant = systemApkVariants(result).get(0); + try (ZipFile systemApkFile = + new ZipFile( + extractFromApkSetFile( + apkSetFile, + systemVariant.getApkSet(0).getApkDescriptionList().get(0).getPath(), + outputDir))) { + assertThat(systemApkFile).hasFile("lib/riscv64/libsome.so"); + assertThat(systemApkFile).doesNotHaveFile("lib/x86/libsome.so"); + assertThat(systemApkFile).doesNotHaveFile("lib/x86_64/libsome.so"); + } + } + @Test public void buildApksCommand_standalone_mixedTargeting() throws Exception { AppBundle appBundle = @@ -4572,10 +4629,13 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() "base", builder -> builder + .addFile("lib/riscv64/libsome.so") .addFile("lib/x86/libsome.so") .addFile("lib/x86_64/libsome.so") .setNativeConfig( nativeLibraries( + targetedNativeDirectory( + "lib/riscv64", nativeDirectoryTargeting(RISCV64)), targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), targetedNativeDirectory( "lib/x86_64", nativeDirectoryTargeting(X86_64)))) @@ -4594,8 +4654,8 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); - // 2 standalone APK variants, 1 split APK variant - assertThat(result.getVariantList()).hasSize(4); + // 3 standalone APK variants, 2 split APK variant + assertThat(result.getVariantList()).hasSize(5); VariantTargeting lSplitVariantTargeting = variantSdkTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, M_SDK_VERSION)); @@ -4603,11 +4663,15 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() variantSdkTargeting(M_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION, L_SDK_VERSION)); VariantTargeting standaloneX86VariantTargeting = mergeVariantTargeting( - variantAbiTargeting(X86, ImmutableSet.of(X86_64)), + variantAbiTargeting(X86, ImmutableSet.of(X86_64, RISCV64)), variantSdkTargeting(LOWEST_SDK_VERSION, ImmutableSet.of(L_SDK_VERSION, M_SDK_VERSION))); VariantTargeting standaloneX64VariantTargeting = mergeVariantTargeting( - variantAbiTargeting(X86_64, ImmutableSet.of(X86)), + variantAbiTargeting(X86_64, ImmutableSet.of(X86, RISCV64)), + variantSdkTargeting(LOWEST_SDK_VERSION, ImmutableSet.of(L_SDK_VERSION, M_SDK_VERSION))); + VariantTargeting standaloneRiscV64VariantTargeting = + mergeVariantTargeting( + variantAbiTargeting(RISCV64, ImmutableSet.of(X86, X86_64)), variantSdkTargeting(LOWEST_SDK_VERSION, ImmutableSet.of(L_SDK_VERSION, M_SDK_VERSION))); ImmutableMap variantsByTargeting = @@ -4621,19 +4685,24 @@ public void buildApksCommand_generateAll_populatesAlternativeVariantTargeting() "base-master.apk", "base-x86.apk", "base-x86_64.apk", + "base-riscv64.apk", "base-master_2.apk", "base-x86_2.apk", - "base-x86_64_2.apk"); + "base-x86_64_2.apk", + "base-riscv64_2.apk"); assertThat(variantsByTargeting.keySet()) .containsExactly( lSplitVariantTargeting, mSplitVariantTargeting, standaloneX86VariantTargeting, - standaloneX64VariantTargeting); + standaloneX64VariantTargeting, + standaloneRiscV64VariantTargeting); assertThat(apkNamesInVariant(variantsByTargeting.get(standaloneX86VariantTargeting))) .containsExactly("standalone-x86.apk"); assertThat(apkNamesInVariant(variantsByTargeting.get(standaloneX64VariantTargeting))) .containsExactly("standalone-x86_64.apk"); + assertThat(apkNamesInVariant(variantsByTargeting.get(standaloneRiscV64VariantTargeting))) + .containsExactly("standalone-riscv64.apk"); } @Test @@ -5296,6 +5365,12 @@ public void splitFileNames_abi() throws Exception { .setNativeConfig( nativeLibraries( targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86))))) + .setBundleConfig( + BundleConfigBuilder.create() + .setUncompressNativeLibraries(true) + .setUncompressDexFiles(true) + .setUncompressDexFilesForVariant(UncompressedDexTargetSdk.SDK_31) + .build()) .build(); TestComponent.useTestModule( this, @@ -5361,6 +5436,11 @@ public void splitNames_assetLanguages() throws Exception { targetedAssetsDirectory( "assets/paks", assetsDirectoryTargeting(alternativeLanguageTargeting("es")))))) + .setBundleConfig( + BundleConfigBuilder.create() + .setUncompressDexFiles(true) + .setUncompressDexFilesForVariant(UncompressedDexTargetSdk.SDK_31) + .build()) .build(); TestComponent.useTestModule( this, @@ -6353,6 +6433,8 @@ public void buildApksCommand_populatesDependencies() throws Exception { withUsesSplit("feature1", "feature2"), withOnDemandAttribute(false), withFusingAttribute(true)))) + // Set old version of Bundletool where this setup wouldn't be merged. + .setBundleConfig(BundleConfigBuilder.create().setVersion("0.14.0").build()) .build(); TestComponent.useTestModule( this, diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksPreprocessingTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksPreprocessingTest.java index e408704a..1772f5fd 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksPreprocessingTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksPreprocessingTest.java @@ -150,7 +150,10 @@ public void renderscript32Bit_64BitStandaloneAndSplitApksFilteredOut() throws Ex new AppBundleSerializer().writeToDisk(appBundle, bundlePath); BuildApksCommand command = - BuildApksCommand.builder().setBundlePath(bundlePath).setOutputFile(outputFilePath).build(); + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .build(); command.execute(); BuildApksResult result; @@ -192,7 +195,10 @@ public void renderscript32Bit_64BitLibsOnly_throws() throws Exception { new AppBundleSerializer().writeToDisk(appBundle, bundlePath); BuildApksCommand command = - BuildApksCommand.builder().setBundlePath(bundlePath).setOutputFile(outputFilePath).build(); + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .build(); InvalidBundleException exception = assertThrows(InvalidBundleException.class, command::execute); assertThat(exception) diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java index 88f5bcc7..66c14456 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java @@ -291,6 +291,67 @@ public void validModule() throws Exception { } } + @Test + public void module_riscV64Arch() throws Exception { + XmlNode manifest = androidManifest(PKG_NAME, withHasCode(true)); + ResourceTable resourceTable = + new ResourceTableBuilder() + .addPackage(PKG_NAME) + .addDrawableResource("icon", "res/drawable/icon.png") + .build(); + Path module = + new ZipBuilder() + .addFileWithContent(ZipPath.create("dex/classes.dex"), "dex".getBytes(UTF_8)) + .addFileWithContent(ZipPath.create("lib/armeabi-v7a/libX.so"), "arm32".getBytes(UTF_8)) + .addFileWithContent(ZipPath.create("lib/arm64-v8a/libX.so"), "arm64".getBytes(UTF_8)) + .addFileWithContent(ZipPath.create("lib/riscv64/libX.so"), "riscv64".getBytes(UTF_8)) + .addFileWithProtoContent(ZipPath.create("manifest/AndroidManifest.xml"), manifest) + .addFileWithContent(ZipPath.create("res/drawable/icon.png"), "image".getBytes(UTF_8)) + .addFileWithProtoContent(ZipPath.create("resources.pb"), resourceTable) + .writeTo(tmpDir.resolve("base.zip")); + NativeLibraries nativeLibraries = + NativeLibraries.newBuilder() + .addDirectory( + TargetedNativeDirectory.newBuilder() + .setPath("lib/armeabi-v7a") + .setTargeting( + NativeDirectoryTargeting.newBuilder() + .setAbi(Abi.newBuilder().setAlias(ARMEABI_V7A)))) + .addDirectory( + TargetedNativeDirectory.newBuilder() + .setPath("lib/arm64-v8a") + .setTargeting( + NativeDirectoryTargeting.newBuilder() + .setAbi(Abi.newBuilder().setAlias(ARM64_V8A)))) + .addDirectory( + TargetedNativeDirectory.newBuilder() + .setPath("lib/riscv64") + .setTargeting( + NativeDirectoryTargeting.newBuilder() + .setAbi(Abi.newBuilder().setAlias(AbiAlias.RISCV64)))) + .build(); + BuildBundleCommand.builder() + .setOutputPath(bundlePath) + .setModulesPaths(ImmutableList.of(module)) + .build() + .execute(); + + try (ZipFile bundle = new ZipFile(bundlePath.toFile())) { + assertThat(bundle).hasFile("base/dex/classes.dex").withContent("dex".getBytes(UTF_8)); + assertThat(bundle) + .hasFile("base/lib/armeabi-v7a/libX.so") + .withContent("arm32".getBytes(UTF_8)); + assertThat(bundle).hasFile("base/lib/arm64-v8a/libX.so").withContent("arm64".getBytes(UTF_8)); + assertThat(bundle).hasFile("base/lib/riscv64/libX.so").withContent("riscv64".getBytes(UTF_8)); + assertThat(bundle) + .hasFile("base/manifest/AndroidManifest.xml") + .withContent(manifest.toByteArray()); + assertThat(bundle).hasFile("base/res/drawable/icon.png").withContent("image".getBytes(UTF_8)); + assertThat(bundle).hasFile("base/resources.pb").withContent(resourceTable.toByteArray()); + assertThat(bundle).hasFile("base/native.pb").withContent(nativeLibraries.toByteArray()); + } + } + @Test public void validApexModule() throws Exception { XmlNode manifest = androidManifest(PKG_NAME, withHasCode(false)); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java index ec495ef6..dfebd36c 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java @@ -20,12 +20,14 @@ import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_SERIAL; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.sdkVersionBuilder; import static com.android.tools.build.bundletool.testing.TestUtils.addKeyToKeystore; import static com.android.tools.build.bundletool.testing.TestUtils.createDebugKeystore; import static com.android.tools.build.bundletool.testing.TestUtils.createKeystore; import static com.android.tools.build.bundletool.testing.TestUtils.createSdkAndroidManifest; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModules; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModulesWithInvalidManifest; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkAsarWithModules; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkBundleWithModules; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; @@ -37,8 +39,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Config.BundleConfig; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; import com.android.tools.build.bundletool.commands.BuildSdkApksCommand.OutputFormat; -import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.flags.FlagParser.FlagParseException; import com.android.tools.build.bundletool.flags.ParsedFlags; @@ -54,6 +56,7 @@ import com.android.tools.build.bundletool.testing.BundleConfigBuilder; import com.android.tools.build.bundletool.testing.CertificateFactory; import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; +import com.android.tools.build.bundletool.testing.SdkBundleBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; @@ -96,6 +99,7 @@ public class BuildSdkApksCommandTest { private Path tmpDir; private Path sdkBundlePath; + private Path sdkAsarPath; private Path modulesPath; private Path outputFilePath; private Path keystorePath; @@ -116,6 +120,7 @@ public static void setUpClass() throws Exception { public void setUp() throws Exception { tmpDir = tmp.getRoot().toPath(); sdkBundlePath = tmpDir.resolve("SdkBundle.asb"); + sdkAsarPath = tmpDir.resolve("SdkArchive.asar"); modulesPath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); outputFilePath = tmpDir.resolve("output.apks"); @@ -127,10 +132,11 @@ public void setUp() throws Exception { } @Test - public void buildingViaFlagsAndBuilderHasSameResult() { + public void buildingViaFlagsAndBuilderHasSameResult_withSdkBundle() { BuildSdkApksCommand commandViaFlags = BuildSdkApksCommand.fromFlags( getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--version-code=351", "--output-format=DIRECTORY", "--aapt2=path/to/aapt2", @@ -157,12 +163,123 @@ public void buildingViaFlagsAndBuilderHasSameResult() { } @Test - public void buildingCommandViaFlags_sdkBundlePathNotSet() { + public void buildingViaFlagsAndBuilderHasSameResult_withSdkArchive() { + BuildSdkApksCommand commandViaFlags = + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--sdk-archive=" + sdkAsarPath, + "--version-code=351", + "--output-format=DIRECTORY", + "--aapt2=path/to/aapt2", + "--ks=" + keystorePath, + "--ks-key-alias=" + KEY_ALIAS, + "--ks-pass=pass:" + KEYSTORE_PASSWORD, + "--key-pass=pass:" + KEY_PASSWORD, + "--verbose")); + + BuildSdkApksCommand.Builder commandViaBuilder = + BuildSdkApksCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setOutputFile(outputFilePath) + .setVersionCode(351) + .setOutputFormat(OutputFormat.DIRECTORY) + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setSigningConfiguration( + SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build()) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setVerbose(true); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void buildingCommandViaFlags_sdkBundlePathSet_sdkArchivePathSet() { + Throwable e = + assertThrows( + IllegalStateException.class, + () -> + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--sdk-archive=" + sdkAsarPath))); + assertThat(e) + .hasMessageThat() + .contains("One and only one of SdkBundlePath and SdkArchivePath should be set."); + } + + @Test + public void buildingCommandViaFlags_sdkBundlePathNotSet_sdkArchivePathNotSet() { + Throwable e = + assertThrows( + IllegalStateException.class, + () -> BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags())); + assertThat(e) + .hasMessageThat() + .contains("One and only one of SdkBundlePath and SdkArchivePath should be set."); + } + + @Test + public void sdkBundleFileDoesNotExist_throws() { Throwable e = assertThrows( - RequiredFlagNotSetException.class, - () -> BuildSdkApksCommand.fromFlags(new FlagParser().parse(""))); - assertThat(e).hasMessageThat().contains("Missing the required --sdk-bundle flag"); + IllegalArgumentException.class, + () -> + BuildSdkApksCommand.builder() + .setSdkBundlePath(Path.of("non_existent.asb")) + .setOutputFile(outputFilePath) + .build() + .execute()); + assertThat(e).hasMessageThat().contains("File 'non_existent.asb' was not found."); + } + + @Test + public void sdkBundleFileHasBadExtension_throws() throws Exception { + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) + .writeTo(sdkAsarPath); + Throwable e = + assertThrows( + IllegalArgumentException.class, + () -> + BuildSdkApksCommand.builder() + .setSdkBundlePath(sdkAsarPath) + .setOutputFile(outputFilePath) + .build() + .execute()); + assertThat(e) + .hasMessageThat() + .contains("ASB file 'SdkArchive.asar' is expected to have '.asb' extension."); + } + + @Test + public void sdkArchiveFileDoesNotExist_throws() { + Throwable e = + assertThrows( + IllegalArgumentException.class, + () -> + BuildSdkApksCommand.builder() + .setSdkArchivePath(Path.of("non_existent.asar")) + .setOutputFile(outputFilePath) + .build() + .execute()); + assertThat(e).hasMessageThat().contains("File 'non_existent.asar' was not found."); + } + + @Test + public void sdkArchiveFileHasBadExtension_throws() throws Exception { + createZipBuilderForSdkAsarWithModules(createZipBuilderForModules(), modulesPath) + .writeTo(sdkBundlePath); + Throwable e = + assertThrows( + IllegalArgumentException.class, + () -> + BuildSdkApksCommand.builder() + .setSdkArchivePath(sdkBundlePath) + .setOutputFile(outputFilePath) + .build() + .execute()); + assertThat(e) + .hasMessageThat() + .contains("ASAR file 'SdkBundle.asb' is expected to have '.asar' extension."); } @Test @@ -179,13 +296,6 @@ public void outputNotSetViaBuilder_throws() { "outputFile", () -> BuildSdkApksCommand.builder().setSdkBundlePath(sdkBundlePath).build()); } - @Test - public void bundleNotSetViaFlags_throws() { - expectMissingRequiredFlagException( - "sdk-bundle", - () -> BuildSdkApksCommand.fromFlags(new FlagParser().parse("--output=" + outputFilePath))); - } - @Test public void keystoreSet_keyAliasNotSet_throws() { InvalidCommandException e = @@ -193,7 +303,8 @@ public void keystoreSet_keyAliasNotSet_throws() { InvalidCommandException.class, () -> BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags("--ks=" + keystorePath))); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--ks=" + keystorePath))); assertThat(e).hasMessageThat().isEqualTo("Flag --ks-key-alias is required when --ks is set."); } @@ -204,22 +315,18 @@ public void keyAliasSet_keystoreNotSet_throws() { InvalidCommandException.class, () -> BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags("--ks-key-alias=" + KEY_ALIAS))); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--ks-key-alias=" + KEY_ALIAS))); assertThat(e).hasMessageThat().isEqualTo("Flag --ks is required when --ks-key-alias is set."); } - @Test - public void bundleNotSetViaBuilder_throws() { - expectMissingRequiredBuilderPropertyException( - "sdkBundlePath", () -> BuildSdkApksCommand.builder().setOutputFile(outputFilePath).build()); - } - @Test public void overwriteSetForDirectoryOutputFormat_throws() throws Exception { createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) .writeTo(sdkBundlePath); ParsedFlags flags = - getDefaultFlagsWithAdditionalFlags("--overwrite", "--output-format=directory"); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--overwrite", "--output-format=directory"); BuildSdkApksCommand command = BuildSdkApksCommand.fromFlags(flags); Exception e = assertThrows(InvalidCommandException.class, command::execute); @@ -234,7 +341,8 @@ public void overwriteNotSetOutputFileAlreadyExists_throws() throws Exception { .addFileWithContent(ZipPath.create("BundleConfig.pb"), BUNDLE_CONFIG.toByteArray()) .writeTo(outputFilePath); BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)); Exception e = assertThrows(IllegalArgumentException.class, command::execute); assertThat(e).hasMessageThat().contains("already exists"); @@ -247,7 +355,8 @@ public void nonPositiveMaxThreads_throws() throws Exception { FlagParseException.class, () -> BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags("--max-threads=0"))); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--max-threads=0"))); assertThat(zeroException).hasMessageThat().contains("flag --max-threads has illegal value"); FlagParseException negativeException = @@ -255,7 +364,8 @@ public void nonPositiveMaxThreads_throws() throws Exception { FlagParseException.class, () -> BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags("--max-threads=-3"))); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--max-threads=-3"))); assertThat(negativeException).hasMessageThat().contains("flag --max-threads has illegal value"); } @@ -266,29 +376,22 @@ public void unknownFlag_throws() { UnknownFlagsException.class, () -> BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags("--unknownFlag=notSure"))); + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--unknownFlag=notSure"))); assertThat(exception) .hasMessageThat() .contains(String.format("Unrecognized flags: --%s", "unknownFlag")); } - @Test - public void missingBundleFile_throws() { - BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); - - Exception e = assertThrows(IllegalArgumentException.class, command::execute); - assertThat(e).hasMessageThat().contains("not found"); - } - // Ensures that validations are run on the bundle zip file. @Test public void bundleMissingFiles_throws() throws Exception { ZipBuilder zipBuilder = new ZipBuilder(); zipBuilder.writeTo(sdkBundlePath); BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)); Exception e = assertThrows(InvalidBundleException.class, command::execute); assertThat(e) @@ -310,7 +413,25 @@ public void bundleMultipleModules_throws() throws Exception { createZipBuilderForSdkBundleWithModules(modules, modulesPath).writeTo(sdkBundlePath); BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e).hasMessageThat().contains("SDK bundles need exactly one module"); + } + + @Test + public void asarMultipleModules_throws() throws Exception { + ZipBuilder modules = + createZipBuilderForModules() + .addFileWithProtoContent( + ZipPath.create("feature/manifest/AndroidManifest.xml"), createSdkAndroidManifest()) + .addFileWithContent(ZipPath.create("feature/dex/classes.dex"), TEST_CONTENT); + createZipBuilderForSdkAsarWithModules(modules, modulesPath).writeTo(sdkAsarPath); + + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-archive=" + sdkAsarPath)); Exception e = assertThrows(InvalidBundleException.class, command::execute); assertThat(e).hasMessageThat().contains("SDK bundles need exactly one module"); @@ -318,12 +439,34 @@ public void bundleMultipleModules_throws() throws Exception { // Ensures that validations are run on the bundle object. @Test - public void invalidManifest_throws() throws Exception { + public void invalidManifest_inSdkBundle_throws() throws Exception { createZipBuilderForSdkBundleWithModules( createZipBuilderForModulesWithInvalidManifest(), modulesPath) .writeTo(sdkBundlePath); BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "'installLocation' in must be 'internalOnly' for SDK bundles if it is set."); + } + + @Test + public void invalidManifest_inSdkArchive_throws() throws Exception { + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModulesWithInvalidManifest(), + SdkMetadata.newBuilder() + .setPackageName(SdkBundleBuilder.PACKAGE_NAME) + .setSdkVersion(sdkVersionBuilder()) + .build(), + modulesPath) + .writeTo(sdkAsarPath); + BuildSdkApksCommand command = + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-archive=" + sdkAsarPath)); Exception e = assertThrows(InvalidBundleException.class, command::execute); assertThat(e) @@ -333,10 +476,30 @@ public void invalidManifest_throws() throws Exception { } @Test - public void executeCreatesFile() throws Exception { + public void executeCreatesFile_fromSdkBundle() throws Exception { createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) .writeTo(sdkBundlePath); - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()).execute(); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)) + .execute(); + assertThat(Files.exists(outputFilePath)).isTrue(); + } + + @Test + public void executeCreatesFile_fromSdkArchive() throws Exception { + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModules(), + SdkMetadata.newBuilder() + .setPackageName(SdkBundleBuilder.PACKAGE_NAME) + .setSdkVersion(sdkVersionBuilder()) + .build(), + modulesPath) + .writeTo(sdkAsarPath); + + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-archive=" + sdkAsarPath)) + .execute(); + assertThat(Files.exists(outputFilePath)).isTrue(); } @@ -345,7 +508,10 @@ public void executeReturnsOutputFile() throws Exception { createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) .writeTo(sdkBundlePath); - assertThat(BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()).execute()) + assertThat( + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)) + .execute()) .isEqualTo(outputFilePath); } @@ -354,7 +520,9 @@ public void internalExecutorIsShutDownAfterExecute() throws Exception { createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) .writeTo(sdkBundlePath); BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags("--max-threads=16")); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--max-threads=16")); command.execute(); assertThat(command.getExecutorService().isShutdown()).isTrue(); @@ -392,7 +560,9 @@ public void noKeystoreProvidedPrintsWarning() throws Exception { try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags(), outputPrintStream, provider); + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath), + outputPrintStream, + provider); assertThat(new String(outputByteArrayStream.toByteArray(), UTF_8)) .contains("WARNING: The APKs won't be signed"); @@ -414,7 +584,9 @@ public void noKeystoreProvidedPrintsInfo_debugKeystore() throws Exception { try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { BuildSdkApksCommand.fromFlags( - getDefaultFlagsWithAdditionalFlags(), outputPrintStream, provider); + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath), + outputPrintStream, + provider); assertThat(new String(outputByteArrayStream.toByteArray(), UTF_8)) .contains("INFO: The APKs will be signed with the debug keystore"); @@ -427,6 +599,7 @@ public void keystoreProvidedDoesNotPrint() throws Exception { PrintStream outputPrintStream = new PrintStream(outputByteArrayStream)) { BuildSdkApksCommand.fromFlags( getDefaultFlagsWithAdditionalFlags( + "--sdk-bundle=" + sdkBundlePath, "--ks=" + keystorePath, "--ks-key-alias=" + KEY_ALIAS, "--ks-pass=pass:" + KEYSTORE_PASSWORD, @@ -441,11 +614,13 @@ public void keystoreProvidedDoesNotPrint() throws Exception { @Test public void verboseIsFalseByDefault() { BuildSdkApksCommand command = - BuildSdkApksCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + BuildSdkApksCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--sdk-bundle=" + sdkBundlePath)); assertThat(command.getVerbose()).isFalse(); } + private ParsedFlags getDefaultFlagsWithAdditionalFlags(String... additionalFlags) { String[] flags = Stream.concat(getDefaultFlagList().stream(), stream(additionalFlags)) @@ -454,6 +629,6 @@ private ParsedFlags getDefaultFlagsWithAdditionalFlags(String... additionalFlags } private ImmutableList getDefaultFlagList() { - return ImmutableList.of("--sdk-bundle=" + sdkBundlePath, "--output=" + outputFilePath); + return ImmutableList.of("--output=" + outputFilePath); } } 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 23d6b70d..6d8ca80d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.commands; import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.RISCV64; 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.ScreenDensity.DensityAlias.XXHDPI; @@ -2277,6 +2278,50 @@ public void incompleteApksFile_missingMatchedAbiSplit_throws(TocFormat tocFormat "Missing APKs for [ABI] dimensions in the module 'base' for the provided device."); } + @Theory + @Test + public void multipleAbiSplits_riscV64(TocFormat tocFormat) throws Exception { + BuildApksResult tableOfContent = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + splitApkDescription( + apkAbiTargeting(RISCV64, ImmutableSet.of(X86_64, X86)), + ZipPath.create("base-riscv64.apk")), + splitApkDescription( + apkAbiTargeting(X86, ImmutableSet.of(X86_64, RISCV64)), + ZipPath.create("base-x86.apk")), + splitApkDescription( + apkAbiTargeting(X86_64, ImmutableSet.of(X86, RISCV64)), + ZipPath.create("base-x86_64.apk"))))) + .build(); + + Path apksArchiveFile = + createApksArchiveFile(tableOfContent, tmpDir.resolve("bundle.apks"), tocFormat); + + DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21), abis("riscv64")); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setDeviceSpec(deviceSpec) + .setApksArchivePath(apksArchiveFile) + .setOutputDirectory(tmpDir) + .build() + .execute(); + assertThat(matchedApks) + .containsExactly( + inOutputDirectory(ZipPath.create("base-master.apk")), + inOutputDirectory(ZipPath.create("base-riscv64.apk"))); + } + @DataPoints("localTestingEnabled") public static final ImmutableSet LOCAL_TESTING_ENABLED = ImmutableSet.of(true, false); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java index cdf49a98..bfcd736a 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java @@ -21,6 +21,7 @@ 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.MIPS64; +import static com.android.bundle.Targeting.Abi.AbiAlias.RISCV64; 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.ScreenDensity.DensityAlias.HDPI; @@ -1117,6 +1118,65 @@ public void getSizeTotal_withDimensionsAndDeviceSpec() throws Exception { + CRLF); } + @Test + public void getSizeTotal_withDimensionsAndDeviceSpec_riscv64() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + createApkDescription( + apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), + ZipPath.create("base-ldpi.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), + ZipPath.create("base-mdpi.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(RISCV64, ImmutableSet.of(X86_64, X86)), + ZipPath.create("base-riscv64.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(X86, ImmutableSet.of(X86_64, RISCV64)), + ZipPath.create("base-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(X86_64, ImmutableSet.of(X86, RISCV64)), + ZipPath.create("base-x86_64.apk"), + /* isMasterSplit= */ false))); + + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec( + DeviceSpec.newBuilder().setScreenDensity(124).addSupportedAbis("riscv64").build()) + .setDimensions(ImmutableSet.of(Dimension.ABI, Dimension.SCREEN_DENSITY)) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + assertThat(new String(outputStream.toByteArray(), UTF_8)) + .isEqualTo( + "ABI,SCREEN_DENSITY,MIN,MAX" + + CRLF + + String.format("riscv64,124,%d,%d", 3 * compressedApkSize, 3 * compressedApkSize) + + CRLF); + } + @Test public void getSizeTotal_withAssetModules() throws Exception { Variant lVariant = diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java index 8ca49d29..cf7fe43f 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java @@ -86,9 +86,9 @@ public class InstallMultiApksCommandTest { private static final String PKG_NAME_1 = "com.example.a"; private static final String PKG_NAME_2 = "com.example.b"; private static final String NONUPDATABLE_PKG_NAME_1 = "com.google.android.permissionconfig"; - private static final String NONUPDATABLE_PKG_NAME_2 = "com.google.android.ext.service"; + private static final String NONUPDATABLE_PKG_NAME_2 = "com.google.android.ext.services"; private static final String NONUPDATABLE_PKG_NAME_3 = "com.google.android.permissioncontroller"; - private static final String NONUPDATABLE_PKG_NAME_4 = "com.google.android.extservice"; + private static final String NONUPDATABLE_PKG_NAME_4 = "com.google.android.extservices"; private static final String NONUPDATABLE_PKG_NAME_5 = "com.google.android.permission"; @Rule public TemporaryFolder tmp = new TemporaryFolder(); diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/BundleModuleMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/BundleModuleMergerTest.java index 65772209..f4f30525 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/BundleModuleMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/BundleModuleMergerTest.java @@ -23,6 +23,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFeatureCondition; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeRemovableElement; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withIsolatedSplits; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandDelivery; @@ -383,7 +384,7 @@ public void testDoNotMergeIfNotInstallTime() throws Exception { @Test public void testDoNotMergeIfConditionalModule() throws Exception { XmlNode conditionalModuleManifest = - androidManifest( + androidManifestForFeature( "com.test.app.detail", withMinSdkVersion(24), withFeatureCondition("android.feature")); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) @@ -526,6 +527,32 @@ public void fuseOnlyActivitiesInManifest_1_0_0() throws Exception { } } + @Test + public void applicationWithIsolatedSplits_noInstallModuleFusing() throws Exception { + XmlNode baseModuleManifest = androidManifest("com.test.app.detail", withIsolatedSplits(true)); + XmlNode installTimeModuleManifest = + androidManifestForFeature( + "com.test.app.detail", + withInstallTimeDelivery(), + withSplitNameActivity("activity1", "detail"), + withSplitNameService("service", "detail")); + createBasicZipBuilder(BUNDLE_CONFIG_1_8_0) + .addFileWithProtoContent( + ZipPath.create("base/manifest/AndroidManifest.xml"), baseModuleManifest) + .addFileWithProtoContent( + ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) + .writeTo(bundleFile); + + try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { + AppBundle appBundle = + BundleModuleMerger.mergeNonRemovableInstallTimeModules( + AppBundle.buildFromZip(appBundleZip), /* overrideBundleToolVersion= */ false); + + assertThat(appBundle.getModules().keySet()) + .containsExactly(BundleModuleName.BASE_MODULE_NAME, BundleModuleName.create("detail")); + } + } + private static Correspondence equalsBundleModuleName() { return Correspondence.from( (BundleModuleName bundleModuleName, String moduleName) -> 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 45955ff6..a8462fb8 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -54,8 +54,10 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withActivityAlias; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withCustomThemeActivity; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFastFollowDelivery; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFeatureCondition; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFusingAttribute; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeDelivery; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeRemovableElement; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstant; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstantInstallTimeDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstantOnDemandDelivery; @@ -1515,4 +1517,80 @@ public void getUsesFeatureElement_absent() { assertThat(androidManifest.getUsesFeatureElement("featureName")).isEmpty(); } + + @Test + public void isAlwaysInstalledModule_legacyOnDemandTrue_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create(androidManifest("com.test.app", withOnDemandAttribute(true))); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_legacyOnDemandFalse_returnsTrue() { + AndroidManifest manifest = + AndroidManifest.create(androidManifest("com.test.app", withOnDemandAttribute(false))); + assertThat(manifest.isAlwaysInstalledModule()).isTrue(); + } + + @Test + public void isAlwaysInstalledModule_noDeliveryElements_returnsTrue() { + AndroidManifest manifest = AndroidManifest.create(xmlNode(xmlElement("manifest"))); + assertThat(manifest.isAlwaysInstalledModule()).isTrue(); + } + + @Test + public void isAlwaysInstalledModule_moduleConditions_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create( + androidManifest("com.test.app", withFeatureCondition("com.feature1"))); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_minSdkCondition_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create(androidManifest("com.test.app", withMinSdkCondition(21))); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_onDemandElement_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create(androidManifest("com.test.app", withOnDemandDelivery())); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_fastFollow_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create( + androidManifest( + "com.test.app", + withTypeAttribute(MODULE_TYPE_ASSET_VALUE), + withFastFollowDelivery())); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_installTimeRemovableFalse_returnsTrue() { + AndroidManifest manifest = + AndroidManifest.create( + androidManifest("com.test.app", withInstallTimeRemovableElement(false))); + assertThat(manifest.isAlwaysInstalledModule()).isTrue(); + } + + @Test + public void isAlwaysInstalledModule_installTimeRemovableTrue_returnsFalse() { + AndroidManifest manifest = + AndroidManifest.create( + androidManifest("com.test.app", withInstallTimeRemovableElement(true))); + assertThat(manifest.isAlwaysInstalledModule()).isFalse(); + } + + @Test + public void isAlwaysInstalledModule_installTimeNoRemovable_returnsTrue() { + AndroidManifest manifest = + AndroidManifest.create(androidManifest("com.test.app", withInstallTimeDelivery())); + assertThat(manifest.isAlwaysInstalledModule()).isTrue(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java b/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java index e7ce5870..02f7cdde 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleMetadataTest.java @@ -23,6 +23,7 @@ import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; +import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Paths; import org.junit.Test; @@ -119,4 +120,113 @@ public void getModuleEntryForSignedTransparencyFile() { .resolve(BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME)) .build()); } + + @Test + public void addFile_duplicateEntry_throws() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + BundleMetadata.builder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'0'})) + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'1'})) + .build()); + + assertThat(exception).hasMessageThat().contains("Multiple entries with same key"); + } + + @Test + public void addFile_duplicateEntry_keepsLast() throws IOException { + BundleMetadata metadata = + BundleMetadata.builder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'0'})) + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'1'})) + .buildKeepingLast(); + + assertThat(metadata.getFileContentMap().keySet()) + .containsExactly(ZipPath.create("com.namespace/filename")); + assertThat(metadata.getFileContentMap().get(ZipPath.create("com.namespace/filename")).read()) + .isEqualTo(new byte[] {'1'}); + } + + @Test + public void toBuilder() throws IOException { + BundleMetadata metadata = + BundleMetadata.builder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'0'})) + .build() + .toBuilder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename1", + ByteSource.wrap(new byte[] {'1'})) + .build(); + + assertThat(metadata.getFileContentMap().keySet()) + .containsExactly( + ZipPath.create("com.namespace/filename"), ZipPath.create("com.namespace/filename1")); + assertThat(metadata.getFileContentMap().get(ZipPath.create("com.namespace/filename")).read()) + .isEqualTo(new byte[] {'0'}); + assertThat(metadata.getFileContentMap().get(ZipPath.create("com.namespace/filename1")).read()) + .isEqualTo(new byte[] {'1'}); + } + + @Test + public void toBuilder_duplicateEntry_throw() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + BundleMetadata.builder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'0'})) + .build() + .toBuilder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'1'})) + .build()); + + assertThat(exception).hasMessageThat().contains("Multiple entries with same key"); + } + + @Test + public void toBuilder_duplicateEntry_keepsLast() throws IOException { + BundleMetadata metadata = + BundleMetadata.builder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'0'})) + .build() + .toBuilder() + .addFile( + /* namespacedDir= */ "com.namespace", + /* fileName= */ "filename", + ByteSource.wrap(new byte[] {'1'})) + .buildKeepingLast(); + + assertThat(metadata.getFileContentMap().keySet()) + .containsExactly(ZipPath.create("com.namespace/filename")); + assertThat(metadata.getFileContentMap().get(ZipPath.create("com.namespace/filename")).read()) + .isEqualTo(new byte[] {'1'}); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java index 58ce3369..e5cba033 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java @@ -24,7 +24,6 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFeatureConditionHexVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFusingAttribute; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeDelivery; -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeRemovableElement; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstantInstallTimeDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstantOnDemandDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMaxSdkCondition; @@ -43,7 +42,6 @@ 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.Version; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.util.Optional; @@ -56,7 +54,6 @@ public class ManifestDeliveryElementTest { private static final String DISTRIBUTION_NAMESPACE_URI = "http://schemas.android.com/apk/distribution"; - private static final Version VERSION = Version.of("1.0.0"); @Test public void emptyDeliveryElement_notWellFormed() { @@ -826,84 +823,6 @@ public void getModuleConditions_multipleUserCountriesConditions_throws() { .contains("Multiple '' conditions are not supported."); } - @Test - public void onDemandModule_removable() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withOnDemandDelivery()), - /* isFastFollowAllowed= */ false); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isTrue(); - } - - @Test - public void fastFollowDelivery_removable() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withFastFollowDelivery()), - /* isFastFollowAllowed= */ true); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isTrue(); - } - - @Test - public void conditionalModule_removable() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest( - "com.test.app", withMinSdkVersion(24), withFeatureCondition("android.feature")), - /* isFastFollowAllowed= */ true); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isTrue(); - } - - @Test - public void installTimeModule_nonRemovableImplicit_newBundleToolVersion() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withInstallTimeDelivery()), - /* isFastFollowAllowed= */ false); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isFalse(); - } - - @Test - public void installTimeModule_removableImplicit_oldBundleToolVersion() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withInstallTimeDelivery()), - /* isFastFollowAllowed= */ false); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(Version.of("0.14.0"))).isTrue(); - } - - @Test - public void installTimeModule_nonRemovable() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withInstallTimeRemovableElement(false)), - /* isFastFollowAllowed= */ false); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isFalse(); - } - - @Test - public void installTimeModule_removable() { - Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withInstallTimeRemovableElement(true)), - /* isFastFollowAllowed= */ false); - - assertThat(deliveryElement).isPresent(); - assertThat(deliveryElement.get().isInstallTimeRemovable(VERSION)).isTrue(); - } - private static XmlNode createAndroidManifestWithDeliveryElement( XmlProtoElementBuilder deliveryElement) { return XmlProtoNode.createElementNode( 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 1fea5829..b0717b22 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -45,10 +45,12 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_BY_PRIVACY_SANDBOX_SDK_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_VERSION_MAJOR_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_RESOURCE_ID; @@ -83,6 +85,7 @@ import com.android.tools.build.bundletool.model.manifestelements.Provider; import com.android.tools.build.bundletool.model.manifestelements.Receiver; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; import com.google.common.collect.ImmutableList; @@ -100,6 +103,7 @@ public class ManifestEditorTest { private static final String ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android"; private static final String VALID_CERT_DIGEST = "96:C7:EC:89:3E:69:2A:25:BA:4D:EE:C1:84:E8:33:3F:34:7D:6D:12:26:A1:C1:AA:70:A2:8A:DB:75:3E:02:0A"; + private static final ImmutableList SPLIT_NAMES = ImmutableList.of("config", "language"); @Test public void setMinSdkVersion_nonExistingElement_created() throws Exception { @@ -523,6 +527,136 @@ public void setSplitsRequired_lastInvocationWins() throws Exception { false)); } + @Test + public void setSplitTypes() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = androidManifest.toEditor().setSplitTypes(SPLIT_NAMES).save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setSplitTypes_idempotent() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest.toEditor().setSplitTypes(SPLIT_NAMES).setSplitTypes(SPLIT_NAMES).save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setSplitTypes_sorted() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest.toEditor().setSplitTypes(ImmutableList.of("language", "config")).save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setSplitTypes_lastInvocationWins() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest + .toEditor() + .setSplitTypes(ImmutableList.of("base,feature")) + .setSplitTypes(SPLIT_NAMES) + .save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setRequiredSplitTypes() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest.toEditor().setRequiredSplitTypes(SPLIT_NAMES).save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setRequiredSplitTypes_idempotent() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest + .toEditor() + .setRequiredSplitTypes(SPLIT_NAMES) + .setRequiredSplitTypes(SPLIT_NAMES) + .save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setRequiredSplitTypes_sorted() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest + .toEditor() + .setRequiredSplitTypes(ImmutableList.of("language", "config")) + .save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + + @Test + public void setRequiredSplitTypes_lastInvocationWins() throws Exception { + AndroidManifest androidManifest = createManifestWithApplicationElement(); + + AndroidManifest editedManifest = + androidManifest + .toEditor() + .setRequiredSplitTypes(ImmutableList.of("base,feature")) + .setRequiredSplitTypes(SPLIT_NAMES) + .save(); + + assertThat(editedManifest.getManifestElement().getAttributes()) + .containsExactly( + XmlProtoAttributeBuilder.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .setValueAsString("config,language") + .build()); + } + @Test public void setTargetSandboxVersion() { AndroidManifest androidManifest = createManifestWithApplicationElement(); diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java index dddd9e83..84888341 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java @@ -17,6 +17,9 @@ package com.android.tools.build.bundletool.model; import static com.android.tools.build.bundletool.model.ManifestMutator.withExtractNativeLibs; +import static com.android.tools.build.bundletool.model.ManifestMutator.withProvidedSplitTypes; +import static com.android.tools.build.bundletool.model.ManifestMutator.withRequiredSplitTypes; +import static com.android.tools.build.bundletool.model.ManifestMutator.withSplitsRequired; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.google.common.truth.Truth8.assertThat; @@ -32,6 +35,7 @@ public class ManifestMutatorTest { @Test public void setExtractNativeLibsValue() throws Exception { AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app")); + assertThat(manifest.getExtractNativeLibsValue()).isEmpty(); AndroidManifest editedManifest = manifest.applyMutators(ImmutableList.of(withExtractNativeLibs(false))); @@ -40,4 +44,39 @@ public void setExtractNativeLibsValue() throws Exception { editedManifest = editedManifest.applyMutators(ImmutableList.of(withExtractNativeLibs(true))); assertThat(editedManifest.getExtractNativeLibsValue()).hasValue(true); } + + @Test + public void setSplitsRequiredValue() throws Exception { + AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app")); + assertThat(manifest.getSplitsRequiredValue()).isEmpty(); + + AndroidManifest editedManifest = + manifest.applyMutators(ImmutableList.of(withSplitsRequired(false))); + assertThat(editedManifest.getSplitsRequiredValue()).hasValue(false); + + editedManifest = editedManifest.applyMutators(ImmutableList.of(withSplitsRequired(true))); + assertThat(editedManifest.getSplitsRequiredValue()).hasValue(true); + } + + @Test + public void setProvidedSplitTypesValue() throws Exception { + AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app")); + assertThat(manifest.getProvidedSplitTypesValue()).isEmpty(); + + AndroidManifest editedManifest = + manifest.applyMutators( + ImmutableList.of(withProvidedSplitTypes(ImmutableList.of("a", "b")))); + assertThat(editedManifest.getProvidedSplitTypesValue()).hasValue(ImmutableList.of("a", "b")); + } + + @Test + public void setRequiresSplitTypesValue() throws Exception { + AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app")); + assertThat(manifest.getRequiredSplitTypesValue()).isEmpty(); + + AndroidManifest editedManifest = + manifest.applyMutators( + ImmutableList.of(withRequiredSplitTypes(ImmutableList.of("a", "b")))); + assertThat(editedManifest.getRequiredSplitTypesValue()).hasValue(ImmutableList.of("a", "b")); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java new file mode 100644 index 00000000..e309418e --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.AndroidManifest.DISTRIBUTION_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; +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.CountrySetTargeting; +import com.android.bundle.Targeting.DeviceTierTargeting; +import com.android.bundle.Targeting.ScreenDensityTargeting; +import com.android.bundle.Targeting.TextureCompressionFormatTargeting; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Map.Entry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RequiredSplitTypesInjectorTest { + + @Test + public void writeSplitTypeValidationInManifest_setsRequiredSplitTypesForModule() { + ModuleSplit baseSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setVariantTargeting(lPlusVariantTargeting()) + .setMasterSplit(true) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + ModuleSplit featureSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("feature")) + .setVariantTargeting(lPlusVariantTargeting()) + .setMasterSplit(true) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + + ImmutableList requiredModules = + ImmutableList.of(baseSplit.getModuleName(), featureSplit.getModuleName()); + ImmutableList allSplits = ImmutableList.of(baseSplit, featureSplit); + ImmutableList newSplits = + RequiredSplitTypesInjector.injectSplitTypeValidation(allSplits, requiredModules); + + baseSplit = newSplits.get(0); + assertThat(getProvidedSplitTypes(baseSplit)).isEmpty(); + + featureSplit = newSplits.get(1); + assertThat(getProvidedSplitTypes(featureSplit)).containsExactly("feature__module"); + } + + @Test + public void writeSplitTypeValidationInManifest_setsRequiredSplitTypesForTargeting() { + ImmutableMap targetingTests = + ImmutableMap.of( + apkAbiTargeting(AbiTargeting.getDefaultInstance()), + "base__abi", + apkDensityTargeting(ScreenDensityTargeting.getDefaultInstance()), + "base__density", + apkDeviceTierTargeting(DeviceTierTargeting.getDefaultInstance()), + "base__tier", + apkCountrySetTargeting(CountrySetTargeting.getDefaultInstance()), + "base__countries", + apkTextureTargeting(TextureCompressionFormatTargeting.getDefaultInstance()), + "base__textures"); + + for (Entry entry : targetingTests.entrySet()) { + ModuleSplit baseSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setVariantTargeting(lPlusVariantTargeting()) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setMasterSplit(true) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + ModuleSplit otherSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setVariantTargeting(lPlusVariantTargeting()) + .setApkTargeting(entry.getKey()) + .setMasterSplit(false) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + + ImmutableList requiredModules = ImmutableList.of(baseSplit.getModuleName()); + ImmutableList allSplits = ImmutableList.of(baseSplit, otherSplit); + ImmutableList newSplits = + RequiredSplitTypesInjector.injectSplitTypeValidation(allSplits, requiredModules); + + baseSplit = newSplits.get(0); + assertThat(getProvidedSplitTypes(baseSplit)).isEmpty(); + assertThat(getRequiredSplitTypes(baseSplit)).containsExactly(entry.getValue()); + + otherSplit = newSplits.get(1); + assertThat(getProvidedSplitTypes(otherSplit)).containsExactly(entry.getValue()); + assertThat(getRequiredSplitTypes(otherSplit)).isEmpty(); + } + } + + private static ImmutableList getRequiredSplitTypes(ModuleSplit moduleSplit) { + String value = + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .orElse( + XmlProtoAttribute.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME)) + .getValueAsString(); + if (value.isEmpty()) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(value.split(",")); + } + + private static ImmutableList getProvidedSplitTypes(ModuleSplit moduleSplit) { + String value = + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .get() + .getValueAsString(); + if (value.isEmpty()) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(value.split(",")); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java index d72d2b65..340bd0fa 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleTest.java @@ -18,6 +18,7 @@ import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModules; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkAsarWithModules; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkBundleWithModules; import static com.google.common.truth.Truth8.assertThat; @@ -40,11 +41,13 @@ public class SdkBundleTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); private Path bundleFile; + private Path asarFile; private Path modulesFile; @Before public void setUp() { bundleFile = tmp.getRoot().toPath().resolve("bundle.asb"); + asarFile = tmp.getRoot().toPath().resolve("archive.asar"); modulesFile = tmp.getRoot().toPath().resolve(EXTRACTED_SDK_MODULES_FILE_NAME); } @@ -66,4 +69,22 @@ public void buildFromZipCreatesExpectedEntries() throws Exception { assertThat(sdkBundle.getSdkInterfaceDescriptors()).isPresent(); } } + + @Test + public void buildFromAsarCreatesExpectedEntries() throws Exception { + ZipBuilder modulesBuilder = + createZipBuilderForModules() + .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), TEST_CONTENT); + createZipBuilderForSdkAsarWithModules(modulesBuilder, modulesFile).writeTo(asarFile); + ZipFile sdkAsarZip = new ZipFile(asarFile.toFile()); + ZipFile modulesZip = new ZipFile(modulesFile.toFile()); + SdkAsar sdkAsar = SdkAsar.buildFromZip(sdkAsarZip, modulesZip, modulesFile); + + SdkBundle sdkBundle = SdkBundle.buildFromAsar(sdkAsar, 1); + + assertThat(sdkBundle.getModule().getEntry(ZipPath.create("dex/classes.dex"))).isPresent(); + assertThat(sdkBundle.getModule().getEntry(ZipPath.create("dex/classes2.dex"))).isPresent(); + assertThat(sdkBundle.getSdkInterfaceDescriptors()).isPresent(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java index ba049269..d4d1ab33 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java @@ -69,6 +69,8 @@ import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Config.MasterResources; import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; @@ -85,6 +87,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; import com.android.tools.build.bundletool.testing.AppBundleBuilder; @@ -440,7 +443,7 @@ public void producesTwoApks_withTransparency() throws Exception { splitOptimizations(OptimizationDimension.LANGUAGE)); assertThat(shards).hasSize(2); - + ModuleSplit fusedShard = shards.get(0); assertThat(fusedShard.isBaseModuleSplit()).isTrue(); assertThat(extractPaths(fusedShard.getEntries())) @@ -1159,6 +1162,83 @@ public void appBundleHasRuntimeEnabledSdkDependencies_injectsRuntimeEnabledSdkTa .isPresent(); } + @Test + public void systemApk_withLanguageSplits_pinnedResources() throws Exception { + BundleModule bundleModule = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .setManifest(androidManifest("com.test.app")) + .setResourceTable( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addStringResourceForMultipleLocales( + "string1", + ImmutableMap.of( + /* default locale */ "", "hello", "es", "hola", "fr", "bonjour")) + .addStringResourceForMultipleLocales( + "string2", + ImmutableMap.of( + /* default locale */ "", "hello2", "es", "hola2", "fr", "bonjour2")) + .addStringResourceForMultipleLocales( + "string3", + ImmutableMap.of( + /* default locale */ "", "hello3", "es", "hola3", "fr", "bonjour3")) + .build()) + .build(); + int string3ResourceId = + ResourcesUtils.entries(bundleModule.getResourceTable().get()) + .filter(entry -> entry.getEntry().getName().equals("string3")) + .map(entry -> entry.getResourceId().getFullResourceId()) + .collect(onlyElement()); + + TestComponent.useTestModule( + this, + TestModule.builder() + .withDeviceSpec(mergeSpecs(DEVICE_SPEC, locales("fr"))) + .withBundleConfig( + BundleConfig.newBuilder() + .setMasterResources( + MasterResources.newBuilder() + .addResourceIds(string3ResourceId) + .addResourceNames("string2"))) + .build()); + + ImmutableList shards = + systemApksGenerator.generateSystemApks( + /* modules= */ ImmutableList.of(bundleModule), + /* modulesToFuse= */ ImmutableSet.of(BASE_MODULE_NAME), + splitOptimizations(OptimizationDimension.LANGUAGE)); + + ModuleSplit mainShard = getSystemImageSplit(shards); + // es 'string' resource is missing from resource table, 'string2' and 'string3' are there + // because they are pinned. + assertThat(mainShard.getResourceTable().get()) + .isEqualTo( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addStringResourceForMultipleLocales( + "string1", ImmutableMap.of(/* default locale */ "", "hello", "fr", "bonjour")) + .addStringResourceForMultipleLocales( + "string2", + ImmutableMap.of( + /* default locale */ "", "hello2", "es", "hola2", "fr", "bonjour2")) + .addStringResourceForMultipleLocales( + "string3", + ImmutableMap.of( + /* default locale */ "", "hello3", "es", "hola3", "fr", "bonjour3")) + .build()); + + ModuleSplit esLangShard = Iterables.getOnlyElement(getAdditionalSplits(shards)); + assertThat(esLangShard.getApkTargeting()).isEqualTo(apkLanguageTargeting("es")); + assertThat(esLangShard.getSplitType()).isEqualTo(SplitType.SYSTEM); + assertThat(esLangShard.getResourceTable().get()) + .isEqualTo( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addStringResourceForMultipleLocales("string1", ImmutableMap.of("es", "hola")) + .build()); + } + private static ApkOptimizations splitOptimizations(OptimizationDimension... dimensions) { return ApkOptimizations.builder() .setSplitDimensions(ImmutableSet.copyOf(dimensions)) diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java index 987bcd52..38d7b446 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java @@ -15,10 +15,10 @@ */ package com.android.tools.build.bundletool.splitters; -import static com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector.BINARY_ART_PROFILE_METADATA_NAME; -import static com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector.BINARY_ART_PROFILE_NAME; -import static com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector.LEGACY_METADATA_NAMESPACE; -import static com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector.METADATA_NAMESPACE; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.LEGACY_PROFILE_METADATA_NAMESPACE; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_FILENAME; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_METADATA_FILENAME; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_METADATA_NAMESPACE; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.google.common.collect.MoreCollectors.toOptional; import static com.google.common.truth.Truth.assertThat; @@ -45,21 +45,19 @@ @RunWith(JUnit4.class) public class BinaryArtProfilesInjectorTest { - private static final byte[] BINARY_ART_PROFILE_CONTENT = {1, 2, 3, 4}; - private static final byte[] BINARY_ART_PROFILE_METADATA_CONTENT = {4, 3, 2, 1}; + private static final byte[] PROFILE_CONTENT = {1, 2, 3, 4}; + private static final byte[] PROFILE_METADATA_CONTENT = {4, 3, 2, 1}; @Test public void mainSplitOfTheBaseModule_artProfileInjected() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(PROFILE_CONTENT)) .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_METADATA_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_METADATA_CONTENT)) + PROFILE_METADATA_NAMESPACE, + PROFILE_METADATA_FILENAME, + ByteSource.wrap(PROFILE_METADATA_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); @@ -83,9 +81,8 @@ public void mainSplitOfTheBaseModule_artProfileInjected() { assertThat(result.findEntry(apkProfileMetadataPath).map(ModuleEntry::getForceUncompressed)) .hasValue(true); - assertThat(getEntryContent(result, apkProfilePath)).hasValue(BINARY_ART_PROFILE_CONTENT); - assertThat(getEntryContent(result, apkProfileMetadataPath)) - .hasValue(BINARY_ART_PROFILE_METADATA_CONTENT); + assertThat(getEntryContent(result, apkProfilePath)).hasValue(PROFILE_CONTENT); + assertThat(getEntryContent(result, apkProfileMetadataPath)).hasValue(PROFILE_METADATA_CONTENT); assertThat(getEntryContent(result, ZipPath.create("some.bin"))).hasValue(new byte[] {10, 9, 8}); } @@ -94,13 +91,11 @@ public void standalone_artProfileInjected() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(PROFILE_CONTENT)) .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_METADATA_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_METADATA_CONTENT)) + PROFILE_METADATA_NAMESPACE, + PROFILE_METADATA_FILENAME, + ByteSource.wrap(PROFILE_METADATA_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); @@ -119,9 +114,9 @@ public void standalone_artProfileInjected() { ModuleSplit result = injector.inject(moduleSplit); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.prof"))) - .hasValue(BINARY_ART_PROFILE_CONTENT); + .hasValue(PROFILE_CONTENT); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.profm"))) - .hasValue(BINARY_ART_PROFILE_METADATA_CONTENT); + .hasValue(PROFILE_METADATA_CONTENT); assertThat(getEntryContent(result, ZipPath.create("some.bin"))).hasValue(new byte[] {10, 9, 8}); } @@ -130,13 +125,13 @@ public void mainSplitOfTheBaseModule_legacyNamespace_artProfileInjected() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - LEGACY_METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + LEGACY_PROFILE_METADATA_NAMESPACE, + PROFILE_FILENAME, + ByteSource.wrap(PROFILE_CONTENT)) .addMetadataFile( - LEGACY_METADATA_NAMESPACE, - BINARY_ART_PROFILE_METADATA_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_METADATA_CONTENT)) + LEGACY_PROFILE_METADATA_NAMESPACE, + PROFILE_METADATA_FILENAME, + ByteSource.wrap(PROFILE_METADATA_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); @@ -149,9 +144,9 @@ public void mainSplitOfTheBaseModule_legacyNamespace_artProfileInjected() { ModuleSplit result = injector.inject(moduleSplit); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.prof"))) - .hasValue(BINARY_ART_PROFILE_CONTENT); + .hasValue(PROFILE_CONTENT); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.profm"))) - .hasValue(BINARY_ART_PROFILE_METADATA_CONTENT); + .hasValue(PROFILE_METADATA_CONTENT); } @Test @@ -159,9 +154,7 @@ public void mainSplitOfTheBaseModule_onlyProfile_injected() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(PROFILE_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); @@ -174,7 +167,7 @@ public void mainSplitOfTheBaseModule_onlyProfile_injected() { ModuleSplit result = injector.inject(moduleSplit); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.prof"))) - .hasValue(BINARY_ART_PROFILE_CONTENT); + .hasValue(PROFILE_CONTENT); assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.profm"))).isEmpty(); } @@ -183,9 +176,7 @@ public void configSplitOfTheBaseModule_skipped() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(PROFILE_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); @@ -206,9 +197,7 @@ public void mainSplitOfOtherModule_skipped() { AppBundle appBundle = createAppBundleBuilder() .addMetadataFile( - METADATA_NAMESPACE, - BINARY_ART_PROFILE_NAME, - ByteSource.wrap(BINARY_ART_PROFILE_CONTENT)) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(PROFILE_CONTENT)) .build(); BinaryArtProfilesInjector injector = new BinaryArtProfilesInjector(appBundle); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitterTest.java index ccb62e58..ccb26540 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/DexCompressionSplitterTest.java @@ -16,8 +16,8 @@ package com.android.tools.build.bundletool.splitters; +import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_O_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_P_API_VERSION; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; @@ -38,19 +38,19 @@ public class DexCompressionSplitterTest { @Test - public void dexCompressionSplitter_withQ_withDexFiles() throws Exception { + public void dexCompressionSplitter_withP_withDexFiles() throws Exception { DexCompressionSplitter dexCompressionSplitter = new DexCompressionSplitter(); ImmutableCollection splits = dexCompressionSplitter.split( ModuleSplit.forDex( - createModuleWithDexFile(), variantSdkTargeting(ANDROID_Q_API_VERSION))); + createModuleWithDexFile(), variantSdkTargeting(ANDROID_P_API_VERSION))); assertThat(splits).hasSize(1); ModuleSplit moduleSplit = Iterables.getOnlyElement(splits); assertThat(moduleSplit.getVariantTargeting()) - .isEqualTo(variantSdkTargeting(ANDROID_Q_API_VERSION)); + .isEqualTo(variantSdkTargeting(ANDROID_P_API_VERSION)); assertThat(extractPaths(moduleSplit.getEntries())).containsExactly("dex/classes.dex"); assertThat(moduleSplit.isMasterSplit()).isTrue(); @@ -59,7 +59,7 @@ public void dexCompressionSplitter_withQ_withDexFiles() throws Exception { } @Test - public void dexCompressionSplitter_withQ_noDexFiles() throws Exception { + public void dexCompressionSplitter_withP_noDexFiles() throws Exception { DexCompressionSplitter dexCompressionSplitter = new DexCompressionSplitter(); ModuleSplit moduleSplit = ModuleSplit.forModule( @@ -68,7 +68,7 @@ public void dexCompressionSplitter_withQ_noDexFiles() throws Exception { .addFile("assets/leftover.txt") .setManifest(androidManifest("com.test.app")) .build(), - variantSdkTargeting(ANDROID_Q_API_VERSION)); + variantSdkTargeting(ANDROID_P_API_VERSION)); ImmutableCollection splits = dexCompressionSplitter.split(moduleSplit); @@ -77,7 +77,7 @@ public void dexCompressionSplitter_withQ_noDexFiles() throws Exception { } @Test - public void dexCompressionSplitter_withQ_otherEntriesCompressionUnchanged() throws Exception { + public void dexCompressionSplitter_withP_otherEntriesCompressionUnchanged() throws Exception { DexCompressionSplitter dexCompressionSplitter = new DexCompressionSplitter(); BundleModule bundleModule = createModuleBuilderWithDexFile() @@ -87,14 +87,14 @@ public void dexCompressionSplitter_withQ_otherEntriesCompressionUnchanged() thro ImmutableCollection splits = dexCompressionSplitter.split( - ModuleSplit.forModule(bundleModule, variantSdkTargeting(ANDROID_Q_API_VERSION))); + ModuleSplit.forModule(bundleModule, variantSdkTargeting(ANDROID_P_API_VERSION))); assertThat(splits).hasSize(1); ModuleSplit moduleSplit = Iterables.getOnlyElement(splits); assertThat(moduleSplit.getVariantTargeting()) - .isEqualTo(variantSdkTargeting(ANDROID_Q_API_VERSION)); + .isEqualTo(variantSdkTargeting(ANDROID_P_API_VERSION)); assertThat(extractPaths(moduleSplit.getEntries())) .containsExactly("lib/x86_64/libsome.so", "assets/leftover.txt", "dex/classes.dex"); @@ -108,19 +108,19 @@ public void dexCompressionSplitter_withQ_otherEntriesCompressionUnchanged() thro } @Test - public void dexCompressionSplitter_preQ_withDexFiles() throws Exception { + public void dexCompressionSplitter_preP_withDexFiles() throws Exception { DexCompressionSplitter dexCompressionSplitter = new DexCompressionSplitter(); ImmutableCollection splits = dexCompressionSplitter.split( ModuleSplit.forDex( - createModuleWithDexFile(), variantSdkTargeting(ANDROID_P_API_VERSION))); + createModuleWithDexFile(), variantSdkTargeting(ANDROID_O_API_VERSION))); assertThat(splits).hasSize(1); ModuleSplit moduleSplit = Iterables.getOnlyElement(splits); assertThat(moduleSplit.getVariantTargeting()) - .isEqualTo(variantSdkTargeting(ANDROID_P_API_VERSION)); + .isEqualTo(variantSdkTargeting(ANDROID_O_API_VERSION)); assertThat(extractPaths(moduleSplit.getEntries())).containsExactly("dex/classes.dex"); assertThat(getForceUncompressed(moduleSplit, "dex/classes.dex")).isFalse(); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java index 27adfc19..0ca1f24e 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java @@ -27,6 +27,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.PROPERTY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_SANDBOX_MIN_VERSION; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_FILENAME; +import static com.android.tools.build.bundletool.model.BinaryArtProfileConstants.PROFILE_METADATA_NAMESPACE; import static com.android.tools.build.bundletool.model.ManifestMutator.withExtractNativeLibs; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; import static com.android.tools.build.bundletool.model.OptimizationDimension.COUNTRY_SET; @@ -2303,9 +2305,7 @@ public void binaryArtProfileIsCopied() { new AppBundleBuilder() .addModule(BASE_MODULE) .addMetadataFile( - BinaryArtProfilesInjector.METADATA_NAMESPACE, - BinaryArtProfilesInjector.BINARY_ART_PROFILE_NAME, - ByteSource.wrap(new byte[] {1, 2, 3})) + PROFILE_METADATA_NAMESPACE, PROFILE_FILENAME, ByteSource.wrap(new byte[] {1, 2, 3})) .build(); ImmutableList splits = createAbiAndDensitySplitter(testModule, appBundle).splitModule(); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java index e9bf5bf9..906b3ce6 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java @@ -16,6 +16,11 @@ package com.android.tools.build.bundletool.splitters; +import static com.android.tools.build.bundletool.model.AndroidManifest.DISTRIBUTION_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_RESOURCE_ID; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_M_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; @@ -30,17 +35,25 @@ import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.type; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMinSdkTargeting; +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.countrySetTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.nativeDirectoryTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.nativeLibraries; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkRuntimeVariantTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedNativeDirectory; +import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMinSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.MoreCollectors.onlyElement; 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; @@ -52,6 +65,7 @@ import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.commands.BuildApksModule; import com.android.tools.build.bundletool.commands.CommandScoped; @@ -63,9 +77,11 @@ import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.TestModule; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -74,6 +90,8 @@ import com.google.common.io.ByteSource; import dagger.Component; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.function.Function; import javax.inject.Inject; import org.junit.Before; @@ -166,6 +184,76 @@ public void simpleMultipleModules_withTransparencyFile() throws Exception { assertThat(testModule.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); } + @Test + public void simpleMultipleModules_withRequiredSplitTypes() throws Exception { + TestComponent.useTestModule(this, TestModule.builder().build()); + ImmutableList bundleModule = + ImmutableList.of( + new BundleModuleBuilder("base") + .addFile("assets/leftover.txt") + .setManifest(androidManifest("com.test.app")) + .build(), + new BundleModuleBuilder("test") + .addFile("assets/test.txt") + .setManifest(androidManifest("com.test.app")) + .build()); + + ImmutableList moduleSplits = + splitApksGenerator.generateSplits( + bundleModule, + ApkGenerationConfiguration.builder().setEnableRequiredSplitTypes(true).build()); + + assertThat(moduleSplits).hasSize(2); + ImmutableMap moduleSplitMap = + Maps.uniqueIndex(moduleSplits, split -> split.getModuleName().getName()); + + ModuleSplit baseModule = moduleSplitMap.get("base"); + assertThat(getRequiredSplitTypes(baseModule)).containsExactly("test__module"); + assertThat(getProvidedSplitTypes(baseModule)).isEmpty(); + + ModuleSplit testModule = moduleSplitMap.get("test"); + assertThat(getRequiredSplitTypes(testModule)).isEmpty(); + assertThat(getProvidedSplitTypes(testModule)).containsExactly("test__module"); + + assertConsistentRequiredSplitTypes(moduleSplits); + } + + @Test + public void simpleMultipleModules_withoutRequiredSplitTypes() throws Exception { + TestComponent.useTestModule(this, TestModule.builder().build()); + ImmutableList bundleModule = + ImmutableList.of( + new BundleModuleBuilder("base") + .addFile("assets/leftover.txt") + .setManifest(androidManifest("com.test.app")) + .build(), + new BundleModuleBuilder("test") + .addFile("assets/test.txt") + .setManifest(androidManifest("com.test.app")) + .build()); + + ImmutableList moduleSplits = + splitApksGenerator.generateSplits( + bundleModule, + ApkGenerationConfiguration.builder().setEnableRequiredSplitTypes(false).build()); + + assertThat(moduleSplits).hasSize(2); + for (ModuleSplit moduleSplit : moduleSplits) { + assertThat( + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID)) + .isEmpty(); + assertThat( + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID)) + .isEmpty(); + } + } + @Test public void multipleModules_withOnlyBaseModuleWithNativeLibraries() throws Exception { ImmutableList bundleModule = @@ -189,6 +277,7 @@ public void multipleModules_withOnlyBaseModuleWithNativeLibraries() throws Excep bundleModule, ApkGenerationConfiguration.builder() .setEnableUncompressedNativeLibraries(true) + .setEnableRequiredSplitTypes(true) .build()); VariantTargeting lVariantTargeting = @@ -211,20 +300,31 @@ public void multipleModules_withOnlyBaseModuleWithNativeLibraries() throws Excep assertThat(extractPaths(baseLModule.getEntries())) .containsExactly("assets/leftover.txt", "lib/x86_64/libsome.so"); assertThat(getForceUncompressed(baseLModule, "lib/x86_64/libsome.so")).isFalse(); + assertThat(getRequiredSplitTypes(baseLModule)).containsExactly("test__module"); + assertThat(getProvidedSplitTypes(baseLModule)).isEmpty(); ModuleSplit testLModule = getModuleSplit(moduleSplits, lVariantTargeting, "test"); assertThat(testLModule.getSplitType()).isEqualTo(SplitType.SPLIT); assertThat(extractPaths(testLModule.getEntries())).containsExactly("assets/test.txt"); + assertThat(getRequiredSplitTypes(testLModule)).isEmpty(); + assertThat(getProvidedSplitTypes(testLModule)).containsExactly("test__module"); ModuleSplit baseMModule = getModuleSplit(moduleSplits, mVariantTargeting, "base"); assertThat(baseMModule.getSplitType()).isEqualTo(SplitType.SPLIT); assertThat(extractPaths(baseMModule.getEntries())) .containsExactly("assets/leftover.txt", "lib/x86_64/libsome.so"); assertThat(getForceUncompressed(baseMModule, "lib/x86_64/libsome.so")).isTrue(); + assertThat(getRequiredSplitTypes(baseMModule)).containsExactly("test__module"); + assertThat(getProvidedSplitTypes(baseMModule)).isEmpty(); ModuleSplit testMModule = getModuleSplit(moduleSplits, mVariantTargeting, "test"); assertThat(testMModule.getSplitType()).isEqualTo(SplitType.SPLIT); assertThat(extractPaths(testMModule.getEntries())).containsExactly("assets/test.txt"); + assertThat(getRequiredSplitTypes(testMModule)).isEmpty(); + assertThat(getProvidedSplitTypes(testMModule)).containsExactly("test__module"); + + assertConsistentRequiredSplitTypes(ImmutableList.of(baseLModule, testLModule)); + assertConsistentRequiredSplitTypes(ImmutableList.of(baseMModule, testMModule)); } @Test @@ -252,6 +352,7 @@ public void multipleModules_multipleVariants_withTransparency() throws Exception bundleModule, ApkGenerationConfiguration.builder() .setOptimizationDimensions(ImmutableSet.of(OptimizationDimension.ABI)) + .setEnableRequiredSplitTypes(true) .build()); ApkTargeting minSdkLTargeting = apkMinSdkTargeting(/* minSdkVersion= */ ANDROID_L_API_VERSION); @@ -262,17 +363,29 @@ public void multipleModules_multipleVariants_withTransparency() throws Exception assertThat(moduleSplits).hasSize(3); assertThat(moduleSplits.stream().map(ModuleSplit::getApkTargeting).collect(toImmutableSet())) .containsExactly(minSdkLTargeting, minSdkLWithAbiTargeting); + ModuleSplit mainSplitOfBaseModule = getModuleSplit(moduleSplits, minSdkLTargeting, /* moduleName= */ "base"); assertThat(extractPaths(mainSplitOfBaseModule.getEntries())) .containsExactly("assets/leftover.txt", "META-INF/code_transparency_signed.jwt"); + assertThat(getRequiredSplitTypes(mainSplitOfBaseModule)) + .containsExactly("base__abi", "test__module"); + assertThat(getProvidedSplitTypes(mainSplitOfBaseModule)).isEmpty(); + ModuleSplit abiSplitOfBaseModule = getModuleSplit(moduleSplits, minSdkLWithAbiTargeting, /* moduleName= */ "base"); assertThat(extractPaths(abiSplitOfBaseModule.getEntries())) .containsExactly("lib/x86_64/libsome.so"); + assertThat(getRequiredSplitTypes(abiSplitOfBaseModule)).isEmpty(); + assertThat(getProvidedSplitTypes(abiSplitOfBaseModule)).containsExactly("base__abi"); + ModuleSplit testModule = getModuleSplit(moduleSplits, minSdkLTargeting, /* moduleName= */ "test"); assertThat(extractPaths(testModule.getEntries())).containsExactly("assets/test.txt"); + assertThat(getRequiredSplitTypes(testModule)).isEmpty(); + assertThat(getProvidedSplitTypes(testModule)).containsExactly("test__module"); + + assertConsistentRequiredSplitTypes(moduleSplits); } @Test @@ -634,6 +747,7 @@ public void appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsF appBundleWithRuntimeEnabledSdkDeps.getModules().values().asList(), ApkGenerationConfiguration.builder() .setOptimizationDimensions(ImmutableSet.of(OptimizationDimension.SCREEN_DENSITY)) + .setEnableRequiredSplitTypes(false) .build()); assertThat( @@ -677,6 +791,323 @@ public void appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsF .collect(toImmutableSet()); // 1 main split + 7 config splits: 1 per each screen density. assertThat(sdkRuntimeVariantSplits).hasSize(8); + + for (ModuleSplit moduleSplit : moduleSplits) { + assertThat( + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID)) + .isEmpty(); + assertThat( + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID)) + .isEmpty(); + } + } + + @Test + public void + appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsForSdkModule_requiredSplitTypesSet() { + ResourceTable appResourceTable = + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "drawable", + entry( + 0x01, + "title_image", + fileReference("res/drawable-hdpi/title_image.jpg", HDPI), + fileReference( + "res/drawable/title_image.jpg", Configuration.getDefaultInstance()))))); + ResourceTable sdkResourceTable = + resourceTable( + pkg( + USER_PACKAGE_OFFSET + 1, + "com.test.sdk", + type( + 0x01, + "drawable", + entry( + 0x01, + "title_image", + fileReference("res/drawable-hdpi/title_image.jpg", HDPI), + fileReference( + "res/drawable/title_image.jpg", Configuration.getDefaultInstance()))))); + AppBundle appBundleWithRuntimeEnabledSdkDeps = + new AppBundleBuilder() + .addModule( + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .setResourceTable(appResourceTable) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setCertificateDigest("AA:BB:CC") + .setResourcesPackageId(2)) + .build()) + .setResourcesPackageId(2) + .build()) + .addModule( + new BundleModuleBuilder("comtestsdk") + .setModuleType(ModuleType.SDK_DEPENDENCY_MODULE) + .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) + .setResourcesPackageId(2) + .setManifest(androidManifest("com.test.sdk")) + .setResourceTable(sdkResourceTable) + .build()) + .build(); + TestComponent.useTestModule( + this, TestModule.builder().withAppBundle(appBundleWithRuntimeEnabledSdkDeps).build()); + + ImmutableList moduleSplits = + splitApksGenerator.generateSplits( + appBundleWithRuntimeEnabledSdkDeps.getModules().values().asList(), + ApkGenerationConfiguration.builder() + .setOptimizationDimensions(ImmutableSet.of(OptimizationDimension.SCREEN_DENSITY)) + .setEnableRequiredSplitTypes(true) + .build()); + + assertThat( + moduleSplits.stream().map(ModuleSplit::getVariantTargeting).collect(toImmutableSet())) + .containsExactly( + lPlusVariantTargeting(), sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + ImmutableMap> moduleSplitMap = + moduleSplits.stream() + .collect(toImmutableListMultimap(ModuleSplit::getVariantTargeting, Function.identity())) + .asMap(); + + // L+ SDK module + Collection lPlusVariantSplits = moduleSplitMap.get(lPlusVariantTargeting()); + ImmutableSet lPlusSdkModuleSplits = + lPlusVariantSplits.stream() + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("comtestsdk")) + .collect(toImmutableSet()); + ImmutableSet lPlusBaseModuleSplits = + lPlusVariantSplits.stream() + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("base")) + .collect(toImmutableSet()); + + // SDK just provides itself + ModuleSplit lPlusSdkModuleSplit = Iterables.getOnlyElement(lPlusSdkModuleSplits); + assertThat(getRequiredSplitTypes(lPlusSdkModuleSplit)).isEmpty(); + assertThat(getProvidedSplitTypes(lPlusSdkModuleSplit)).containsExactly("comtestsdk__module"); + + // Base module requires density and the SDK + ModuleSplit lPlusBaseModuleSplit = + lPlusBaseModuleSplits.stream() + .filter(moduleSplit -> !moduleSplit.getApkTargeting().hasScreenDensityTargeting()) + .collect(onlyElement()); + assertThat(getRequiredSplitTypes(lPlusBaseModuleSplit)) + .containsExactly("base__density", "comtestsdk__module"); + assertThat(getProvidedSplitTypes(lPlusBaseModuleSplit)).isEmpty(); + + // Density splits provide density + ImmutableSet lPlusBaseModuleDensitySplits = + lPlusBaseModuleSplits.stream() + .filter(moduleSplit -> moduleSplit.getApkTargeting().hasScreenDensityTargeting()) + .collect(toImmutableSet()); + lPlusBaseModuleDensitySplits.forEach( + split -> { + assertThat(getRequiredSplitTypes(split)).isEmpty(); + assertThat(getProvidedSplitTypes(split)).containsExactly("base__density"); + }); + + assertConsistentRequiredSplitTypes(lPlusVariantSplits); + + // T+ runtime enabled + Collection tPlusVariantSplits = + moduleSplitMap.get(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + ImmutableSet tPlusSdkModuleSplits = + lPlusVariantSplits.stream() + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("comtestsdk")) + .collect(toImmutableSet()); + ImmutableSet tPlusBaseModuleSplits = + lPlusVariantSplits.stream() + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("base")) + .collect(toImmutableSet()); + + // SDK just provides itself + ModuleSplit tPlusSdkModuleSplit = Iterables.getOnlyElement(tPlusSdkModuleSplits); + assertThat(getRequiredSplitTypes(tPlusSdkModuleSplit)).isEmpty(); + assertThat(getProvidedSplitTypes(tPlusSdkModuleSplit)).containsExactly("comtestsdk__module"); + + // Base module requires density and the SDK + ModuleSplit tPlusBaseModuleSplit = + tPlusBaseModuleSplits.stream() + .filter(moduleSplit -> !moduleSplit.getApkTargeting().hasScreenDensityTargeting()) + .collect(onlyElement()); + assertThat(getRequiredSplitTypes(tPlusBaseModuleSplit)) + .containsExactly("base__density", "comtestsdk__module"); + assertThat(getProvidedSplitTypes(tPlusBaseModuleSplit)).isEmpty(); + + // Density splits provide density + ImmutableSet tPlusBaseModuleDensitySplits = + tPlusBaseModuleSplits.stream() + .filter(moduleSplit -> moduleSplit.getApkTargeting().hasScreenDensityTargeting()) + .collect(toImmutableSet()); + tPlusBaseModuleDensitySplits.forEach( + split -> { + assertThat(getRequiredSplitTypes(split)).isEmpty(); + assertThat(getProvidedSplitTypes(split)).containsExactly("base__density"); + }); + + assertConsistentRequiredSplitTypes(tPlusVariantSplits); + } + + @Test + public void appBundleWithAllSplitTargeting_requiredSplitTypesSet() { + ResourceTable appResourceTable = + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "drawable", + entry( + 0x01, + "title_image", + fileReference("res/drawable-hdpi/title_image.jpg", HDPI), + fileReference( + "res/drawable/title_image.jpg", Configuration.getDefaultInstance()))))); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .setResourceTable(appResourceTable) + .setResourcesPackageId(2) + .addFile("assets/languages#lang_es/strings.xml") + .addFile("assets/textures#tcf_atc/textures.dat") + .addFile("assets/textures#tier_0/textures.dat") + .addFile("assets/content#countries_latam/strings.xml") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/languages#lang_es", + assetsDirectoryTargeting(languageTargeting("es"))), + targetedAssetsDirectory( + "assets/textures#tcf_atc", + assetsDirectoryTargeting( + textureCompressionTargeting( + TextureCompressionFormatAlias.ATC))), + targetedAssetsDirectory( + "assets/textures#tier_0", + assetsDirectoryTargeting(deviceTierTargeting(/* value= */ 0))), + targetedAssetsDirectory( + "assets/content#countries_latam", + assetsDirectoryTargeting(countrySetTargeting("latam"))))) + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + .build()) + .addModule( + new BundleModuleBuilder("test") + .addFile("assets/test.txt") + .setManifest(androidManifest("com.test.app")) + .addFile("assets/languages#lang_es/strings.xml") + .addFile("assets/textures#tcf_atc/textures.dat") + .addFile("assets/textures#tier_0/textures.dat") + .addFile("assets/content#countries_latam/strings.xml") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/languages#lang_es", + assetsDirectoryTargeting(languageTargeting("es"))), + targetedAssetsDirectory( + "assets/textures#tcf_atc", + assetsDirectoryTargeting( + textureCompressionTargeting( + TextureCompressionFormatAlias.ATC))), + targetedAssetsDirectory( + "assets/textures#tier_0", + assetsDirectoryTargeting(deviceTierTargeting(/* value= */ 0))), + targetedAssetsDirectory( + "assets/content#countries_latam", + assetsDirectoryTargeting(countrySetTargeting("latam"))))) + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + .build()) + .build(); + + ImmutableList moduleSplits = + splitApksGenerator.generateSplits( + appBundle.getModules().values().asList(), + ApkGenerationConfiguration.builder() + .setOptimizationDimensions( + ImmutableSet.of( + OptimizationDimension.SCREEN_DENSITY, + OptimizationDimension.ABI, + OptimizationDimension.LANGUAGE, + OptimizationDimension.TEXTURE_COMPRESSION_FORMAT, + OptimizationDimension.DEVICE_TIER, + OptimizationDimension.COUNTRY_SET)) + .setEnableRequiredSplitTypes(true) + .build()); + + ModuleSplit baseModule = getModuleSplit(moduleSplits, "base", Optional.empty()); + assertThat(getProvidedSplitTypes(baseModule)).isEmpty(); + assertThat(getRequiredSplitTypes(baseModule)) + .containsExactly( + "base__abi", + "base__countries", + "base__density", + "base__textures", + "base__tier", + "test__module"); + + ModuleSplit testModule = getModuleSplit(moduleSplits, "test", Optional.empty()); + assertThat(getProvidedSplitTypes(testModule)).containsExactly("test__module"); + assertThat(getRequiredSplitTypes(testModule)) + .containsExactly("test__abi", "test__countries", "test__textures", "test__tier"); + + ImmutableMap expectedProvidedSplitTypes = + ImmutableMap.of( + "base.countries_latam", + "base__countries", + "base.ldpi", + "base__density", + "base.x86_64", + "base__abi", + "base.atc", + "base__textures", + "base.tier_0", + "base__tier", + "test.countries_latam", + "test__countries", + "test.x86_64", + "test__abi", + "test.atc", + "test__textures", + "test.tier_0", + "test__tier"); + + expectedProvidedSplitTypes.forEach( + (splitId, splitType) -> { + List parts = Splitter.on(".").splitToList(splitId); + String moduleName = parts.get(0); + String suffix = parts.get(1); + ModuleSplit moduleSplit = getModuleSplit(moduleSplits, moduleName, Optional.of(suffix)); + assertThat(getProvidedSplitTypes(moduleSplit)).containsExactly(splitType); + assertThat(getRequiredSplitTypes(moduleSplit)).isEmpty(); + }); + + ModuleSplit languageSplit = getModuleSplit(moduleSplits, "base", Optional.of("es")); + assertThat(getProvidedSplitTypes(languageSplit)).isEmpty(); + + assertConsistentRequiredSplitTypes(moduleSplits); } private static ModuleSplit getModuleSplit( @@ -699,10 +1130,71 @@ private static ModuleSplit getModuleSplit( .get(); } + private static ModuleSplit getModuleSplit( + ImmutableList moduleSplits, String moduleName, Optional suffix) { + return moduleSplits.stream() + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals(moduleName)) + .filter( + moduleSplit -> + suffix.isEmpty() + ? moduleSplit.getSuffix().isEmpty() + : moduleSplit.getSuffix().equals(suffix.get())) + .findFirst() + .get(); + } + private static boolean getForceUncompressed(ModuleSplit moduleSplit, String path) { return moduleSplit.findEntry(path).get().getForceUncompressed(); } + /** + * Given a list of splits, assert that the provided and required split types are internally + * consistent. + */ + private static void assertConsistentRequiredSplitTypes(Collection moduleSplits) { + ImmutableSet.Builder requiredSplitTypes = ImmutableSet.builder(); + for (ModuleSplit moduleSplit : moduleSplits) { + requiredSplitTypes.addAll(getRequiredSplitTypes(moduleSplit)); + } + + ImmutableSet.Builder providedSplitTypes = ImmutableSet.builder(); + for (ModuleSplit moduleSplit : moduleSplits) { + providedSplitTypes.addAll(getProvidedSplitTypes(moduleSplit)); + } + + assertThat(requiredSplitTypes.build()).containsExactlyElementsIn(providedSplitTypes.build()); + } + + private static ImmutableList getRequiredSplitTypes(ModuleSplit moduleSplit) { + String value = + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .orElse( + XmlProtoAttribute.create( + DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME)) + .getValueAsString(); + if (value.isEmpty()) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(value.split(",")); + } + + private static ImmutableList getProvidedSplitTypes(ModuleSplit moduleSplit) { + String value = + moduleSplit + .getAndroidManifest() + .getManifestElement() + .getAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .get() + .getValueAsString(); + if (value.isEmpty()) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(value.split(",")); + } + @CommandScoped @Component(modules = {BuildApksModule.class, TestModule.class}) interface TestComponent { diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java index f0df129a..6e852d52 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java @@ -577,9 +577,9 @@ public static ManifestMutator withInstantInstallTimeDelivery() { .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time"); } - public static ManifestMutator withDeepLink( + public static ManifestMutator withDeeplink( String activityName, String path, String host, String scheme, boolean addToActivityAlias) { - return withDeepLink( + return withDeeplink( activityName, host, scheme, @@ -588,13 +588,13 @@ public static ManifestMutator withDeepLink( addToActivityAlias); } - public static ManifestMutator withDeepLink( + public static ManifestMutator withDeeplink( String activityName, int pathResourceId, String host, String scheme, boolean addToActivityAlias) { - return withDeepLink( + return withDeeplink( activityName, host, scheme, @@ -603,7 +603,7 @@ public static ManifestMutator withDeepLink( addToActivityAlias); } - private static ManifestMutator withDeepLink( + private static ManifestMutator withDeeplink( String activityName, String host, String scheme, diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java index b5328b4f..f2b34e7b 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java @@ -133,6 +133,7 @@ public static class Builder { @Nullable private PrintStream printStream; @Nullable private Boolean localTestingEnabled; @Nullable private SourceStamp sourceStamp; + private Boolean enableRequiredSplitTypes = true; private BundleMetadata bundleMetadata = DEFAULT_BUNDLE_METADATA; public Builder withAppBundle(AppBundle appBundle) { @@ -261,6 +262,11 @@ public Builder withSdkBundle(SdkBundle sdkBundle) { return this; } + public Builder withEnableRequiredSplitTypes(boolean enableRequiredSplitTypes) { + this.enableRequiredSplitTypes = enableRequiredSplitTypes; + return this; + } + public TestModule build() { try { if (tempDirectory == null) { 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 bb5a9632..fe7312be 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -227,7 +227,8 @@ public static ZipBuilder createZipBuilderForSdkAsar() { public static ZipBuilder createZipBuilderForSdkAsar(SdkMetadata sdkMetadata) { return new ZipBuilder() - .addFileWithProtoContent(ZipPath.create(SDK_METADATA_FILE_NAME), sdkMetadata); + .addFileWithProtoContent(ZipPath.create(SDK_METADATA_FILE_NAME), sdkMetadata) + .addFileWithContent(ZipPath.create(SDK_INTERFACE_DESCRIPTORS_FILE_NAME), TEST_CONTENT); } public static ZipBuilder createZipBuilderForModules() {