From 1082065796c1a67826b6e962d764d51b97f8982f Mon Sep 17 00:00:00 2001 From: Alessandro Dovis Date: Fri, 24 Jun 2022 14:59:38 +0100 Subject: [PATCH] Prepare for release 1.11.0. --- README.md | 2 +- gradle.properties | 2 +- .../build/bundletool/BundleToolMain.java | 14 + .../archive/ArchivedAndroidManifestUtils.java | 25 +- .../bundletool/commands/BuildApksCommand.java | 205 ++++++++- .../bundletool/commands/BuildApksManager.java | 17 +- .../commands/BuildSdkAsarCommand.java | 187 ++++++++ .../commands/BuildSdkAsarManager.java | 85 ++++ .../BuildSdkAsarManagerComponent.java | 39 ++ .../commands/BuildSdkAsarModule.java | 23 + .../commands/BuildSdkBundleCommand.java | 4 +- .../bundletool/device/DeviceAnalyzer.java | 8 +- .../bundletool/device/VariantMatcher.java | 27 +- .../bundletool/io/ModuleEntriesPacker.java | 8 +- .../bundletool/io/ModuleSplitSerializer.java | 2 +- .../bundletool/io/SdkAsarSerializer.java | 49 +++ ...r.java => ZipFlingerBundleSerializer.java} | 165 ++++++- .../bundletool/model/AndroidManifest.java | 29 +- .../bundletool/model/BundleMetadata.java | 6 +- ...zer.java => ClassesDexEntriesMutator.java} | 77 +++- .../build/bundletool/model/ModuleEntry.java | 32 +- .../bundletool/model/ResourceIdRemapper.java | 59 --- .../model/ResourceTablePackageIdRemapper.java | 238 +++++++++++ .../tools/build/bundletool/model/SdkAsar.java | 161 +++++++ ...undleModuleToAppBundleModuleConverter.java | 45 +- .../model/XmlPackageIdRemapper.java | 131 ++++++ .../model/utils/BundleModuleParser.java | 6 + .../bundletool/model/utils/BundleParser.java | 16 +- .../model/utils/ResourcesUtils.java | 27 +- .../model/utils/TargetingProtoUtils.java | 9 +- .../model/version/BundleToolVersion.java | 2 +- .../splitters/SdkRuntimeVariantGenerator.java | 39 +- .../transparency/CodeTransparencyFactory.java | 4 +- .../validation/BundleZipValidator.java | 8 + src/main/proto/sdk_metadata.proto | 21 + .../commands/BuildApksCommandTest.java | 404 +++++++++++++++++- .../commands/BuildApksManagerTest.java | 119 ++++++ .../commands/BuildApksPreprocessingTest.java | 8 +- .../commands/BuildSdkApksCommandTest.java | 4 +- .../commands/BuildSdkAsarCommandTest.java | 271 ++++++++++++ .../commands/BuildSdkAsarManagerTest.java | 245 +++++++++++ .../commands/GetSizeCommandTest.java | 10 +- .../device/AssetModuleSizeAggregatorTest.java | 30 +- .../bundletool/device/DeviceAnalyzerTest.java | 25 +- .../bundletool/device/VariantMatcherTest.java | 62 +++ .../VariantTotalSizeAggregatorTest.java | 2 +- .../mergers/BundleModuleMergerTest.java | 94 ++-- .../ModuleSplitsToShardMergerTest.java | 22 +- .../mergers/ResourceTableMergerTest.java | 11 +- .../mergers/SameTargetingMergerTest.java | 36 +- .../bundletool/model/AndroidManifestTest.java | 55 +-- .../build/bundletool/model/AppBundleTest.java | 80 ++-- .../bundletool/model/BundleMetadataTest.java | 18 +- .../bundletool/model/BundleModuleTest.java | 16 +- .../model/ClassDexNameSanitizerTest.java | 87 ---- .../model/ClassesDexEntriesMutatorTest.java | 146 +++++++ .../bundletool/model/ModuleSplitTest.java | 20 +- .../model/ResourceIdRemapperTest.java | 82 ---- .../ResourceTablePackageIdRemapperTest.java | 198 +++++++++ .../build/bundletool/model/SdkAsarTest.java | 91 ++++ ...eModuleToAppBundleModuleConverterTest.java | 136 +++++- .../build/bundletool/model/SdkBundleTest.java | 6 +- .../model/XmlPackageIdRemapperTest.java | 271 ++++++++++++ .../model/utils/ApkSizeUtilsTest.java | 6 +- .../bundletool/shards/SuffixStripperTest.java | 36 +- .../SdkRuntimeVariantGeneratorTest.java | 48 ++- .../splitters/SplitApksGeneratorTest.java | 2 +- .../testing/ApksArchiveHelpers.java | 6 +- .../build/bundletool/testing/AsarUtils.java | 55 +++ .../testing/BundleModuleBuilder.java | 6 +- .../build/bundletool/testing/FakeDevice.java | 13 +- .../bundletool/testing/TargetingUtils.java | 16 +- .../build/bundletool/testing/TestModule.java | 9 + .../build/bundletool/testing/TestUtils.java | 39 +- .../validation/BundleZipValidatorTest.java | 35 +- .../MandatoryFilesPresenceValidatorTest.java | 16 +- .../SdkBundleHasOneModuleValidatorTest.java | 4 +- .../validation/ValidatorRunnerTest.java | 18 +- 78 files changed, 3929 insertions(+), 701 deletions(-) create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommand.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManager.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerComponent.java create mode 100644 src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarModule.java create mode 100644 src/main/java/com/android/tools/build/bundletool/io/SdkAsarSerializer.java rename src/main/java/com/android/tools/build/bundletool/io/{ZipFlingerAppBundleSerializer.java => ZipFlingerBundleSerializer.java} (52%) rename src/main/java/com/android/tools/build/bundletool/model/{ClassesDexNameSanitizer.java => ClassesDexEntriesMutator.java} (55%) delete mode 100644 src/main/java/com/android/tools/build/bundletool/model/ResourceIdRemapper.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapper.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java create mode 100644 src/main/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapper.java create mode 100644 src/main/proto/sdk_metadata.proto create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommandTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerTest.java delete mode 100644 src/test/java/com/android/tools/build/bundletool/model/ClassDexNameSanitizerTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutatorTest.java delete mode 100644 src/test/java/com/android/tools/build/bundletool/model/ResourceIdRemapperTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapperTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/SdkAsarTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapperTest.java create mode 100644 src/test/java/com/android/tools/build/bundletool/testing/AsarUtils.java diff --git a/README.md b/README.md index 2d17a28f..2f83db83 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.10.1](https://github.com/google/bundletool/releases) +Latest release: [1.11.0](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 2346f246..7369eec9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.10.1 +release_version = 1.11.0 diff --git a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java index 7b2bdb79..172c4cd6 100644 --- a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java +++ b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java @@ -19,6 +19,7 @@ import com.android.tools.build.bundletool.commands.BuildApksCommand; import com.android.tools.build.bundletool.commands.BuildBundleCommand; import com.android.tools.build.bundletool.commands.BuildSdkApksCommand; +import com.android.tools.build.bundletool.commands.BuildSdkAsarCommand; import com.android.tools.build.bundletool.commands.BuildSdkBundleCommand; import com.android.tools.build.bundletool.commands.CheckTransparencyCommand; import com.android.tools.build.bundletool.commands.CommandHelp; @@ -29,6 +30,7 @@ import com.android.tools.build.bundletool.commands.GetSizeCommand; import com.android.tools.build.bundletool.commands.InstallApksCommand; import com.android.tools.build.bundletool.commands.InstallMultiApksCommand; +import com.android.tools.build.bundletool.commands.PrintDeviceTargetingConfigCommand; import com.android.tools.build.bundletool.commands.ValidateBundleCommand; import com.android.tools.build.bundletool.commands.VersionCommand; import com.android.tools.build.bundletool.device.AdbServer; @@ -86,6 +88,9 @@ static void main(String[] args, Runtime runtime) { case BuildSdkApksCommand.COMMAND_NAME: BuildSdkApksCommand.fromFlags(flags).execute(); break; + case BuildSdkAsarCommand.COMMAND_NAME: + BuildSdkAsarCommand.fromFlags(flags).execute(); + break; case ExtractApksCommand.COMMAND_NAME: ExtractApksCommand.fromFlags(flags).execute(); break; @@ -182,6 +187,15 @@ public static void help(String commandName, Runtime runtime) { case BuildApksCommand.COMMAND_NAME: commandHelp = BuildApksCommand.help(); break; + case BuildSdkBundleCommand.COMMAND_NAME: + commandHelp = BuildSdkBundleCommand.help(); + break; + case BuildSdkApksCommand.COMMAND_NAME: + commandHelp = BuildSdkApksCommand.help(); + break; + case BuildSdkAsarCommand.COMMAND_NAME: + commandHelp = BuildSdkAsarCommand.help(); + break; case ExtractApksCommand.COMMAND_NAME: commandHelp = ExtractApksCommand.help(); break; diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java index 8f707947..0990865b 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java @@ -16,7 +16,9 @@ package com.android.tools.build.bundletool.archive; +import static com.android.tools.build.bundletool.model.AndroidManifest.ALLOW_BACKUP_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.BACKUP_AGENT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LAUNCHER_CATEGORY_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LEANBACK_FEATURE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LEANBACK_LAUNCHER_CATEGORY_NAME; @@ -34,7 +36,6 @@ import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.google.common.collect.ImmutableList; -import java.util.Optional; /** Utility methods for creation of archived manifest. */ public final class ArchivedAndroidManifestUtils { @@ -98,7 +99,17 @@ public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { APPLICATION_ATTRIBUTES_TO_KEEP.forEach( attrResourceId -> editor.copyApplicationElementAndroidAttribute(manifest, attrResourceId)); - getArchivedAllowBackup(manifest).ifPresent(editor::setAllowBackup); + + // Backup needs to be disabled if Backup Agent is provided. Custom backup agent cannot be + // kept because it relies on app code that is not present in its archived variant. + // FullBackupOnly attribute value cannot be checked due to complicated and ambigous process + // of resolving attribute value (because boolean type is not being enforced and any other type + // such as resource refrenece can be acceptable). + if (!manifest.hasApplicationAttribute(BACKUP_AGENT_RESOURCE_ID)) { + editor.copyApplicationElementAndroidAttribute(manifest, ALLOW_BACKUP_RESOURCE_ID); + } else { + editor.setAllowBackup(false); + } } manifest @@ -115,16 +126,6 @@ public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { return editor.save(); } - private static Optional getArchivedAllowBackup(AndroidManifest manifest) { - // Backup needs to be disabled if Backup Agent is provided and Full Backup Only is disabled. - // Custom backup agent cannot be kept because it relies on app code that is not present in its - // archived variant. - return manifest.getAllowBackup().orElse(true) - && (!manifest.hasBackupAgent() || manifest.getFullBackupOnly().orElse(false)) - ? manifest.getAllowBackup() - : Optional.of(Boolean.FALSE); - } - private static XmlProtoNode createMinimalManifestTag() { return XmlProtoNode.createElementNode( XmlProtoElementBuilder.create("manifest") diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index 5624721c..29039811 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -54,6 +54,7 @@ import com.android.tools.build.bundletool.model.KeystoreProperties; import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.Password; +import com.android.tools.build.bundletool.model.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; @@ -165,6 +166,8 @@ public enum OutputFormat { private static final Flag> MODULES_FLAG = Flag.stringSet("modules"); private static final Flag DEVICE_SPEC_FLAG = Flag.path("device-spec"); + private static final Flag FUSE_ONLY_DEVICE_MATCHING_MODULES_FLAG = + Flag.booleanFlag("fuse-only-device-matching-modules"); private static final Flag> SYSTEM_APK_OPTIONS = Flag.enumSet("system-apk-options", SystemApkOption.class); private static final Flag DEVICE_TIER_FLAG = Flag.nonNegativeInteger("device-tier"); @@ -198,6 +201,8 @@ public enum OutputFormat { // Runtime-enabled-SDK-related flags. private static final Flag> RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG = Flag.pathSet("sdk-bundles"); + private static final Flag> RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG = + Flag.pathSet("sdk-archives"); // Archive APK related flags. private static final Flag APP_STORE_PACKAGE_NAME_FLAG = Flag.string("store-package"); @@ -222,6 +227,8 @@ public enum OutputFormat { public abstract Optional getDeviceSpec(); + public abstract boolean getFuseOnlyDeviceMatchingModules(); + public abstract Optional getDeviceTier(); public abstract ImmutableSet getSystemApkOptions(); @@ -281,6 +288,8 @@ ListeningExecutorService getExecutorService() { public abstract ImmutableSet getRuntimeEnabledSdkBundlePaths(); + public abstract ImmutableSet getRuntimeEnabledSdkArchivePaths(); + public abstract Optional getAppStorePackageName(); public static Builder builder() { @@ -293,10 +302,12 @@ public static Builder builder() { .setVerbose(false) .setOptimizationDimensions(ImmutableSet.of()) .setModules(ImmutableSet.of()) + .setFuseOnlyDeviceMatchingModules(false) .setExtraValidators(ImmutableList.of()) .setSystemApkOptions(ImmutableSet.of()) .setEnableApkSerializerWithoutBundleRecompression(true) - .setRuntimeEnabledSdkBundlePaths(ImmutableSet.of()); + .setRuntimeEnabledSdkBundlePaths(ImmutableSet.of()) + .setRuntimeEnabledSdkArchivePaths(ImmutableSet.of()); } /** Builder for the {@link BuildApksCommand}. */ @@ -363,6 +374,8 @@ public Builder setDeviceSpec(Path deviceSpecFile) { return setDeviceSpec(DeviceSpecParser.parsePartialDeviceSpec(deviceSpecFile)); } + public abstract Builder setFuseOnlyDeviceMatchingModules(boolean enabled); + /** * Sets the device tier to use for APK matching. This will override the device tier of the given * device spec. @@ -504,9 +517,19 @@ public Builder setCreateApkSetArchive(boolean createApkSetArchive) { /** * Provides paths to {@link SdkBundle}s for the runtime-enabled SDKs that the {@link AppBundle} * depends on. Each file must have extension ".asb". + * + *

Can not be set together with {@link #setRuntimeEnabledSdkArchivePaths(ImmutableSet)}. */ public abstract Builder setRuntimeEnabledSdkBundlePaths(ImmutableSet sdkBundlePaths); + /** + * Provides paths to {@link SdkAsar}s for the runtime-enabled SDKs that the {@link AppBundle} + * depends on. Each file must have extension ".asar". + * + *

Can not be set together with {@link #setRuntimeEnabledSdkBundlePaths(ImmutableSet)}. + */ + public abstract Builder setRuntimeEnabledSdkArchivePaths(ImmutableSet sdkArchivePaths); + /** * Sets package name of an app store that will be called by archived app to redownload the * application. @@ -589,6 +612,14 @@ public BuildApksCommand build() { } } + if (command.getFuseOnlyDeviceMatchingModules() && !command.getDeviceSpec().isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Device spec must be provided when using '%s' flag.", + FUSE_ONLY_DEVICE_MATCHING_MODULES_FLAG.getName()) + .build(); + } + if (command.getGenerateOnlyForConnectedDevice() && command.getDeviceSpec().isPresent()) { throw InvalidCommandException.builder() .withInternalMessage( @@ -641,6 +672,15 @@ public BuildApksCommand build() { ARCHIVE.getLowerCaseName()) .build(); } + + if (!command.getRuntimeEnabledSdkBundlePaths().isEmpty() + && !command.getRuntimeEnabledSdkArchivePaths().isEmpty()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Command can only set either runtime-enabled SDK bundles or runtime-enabled SDK" + + " archives, but both were set.") + .build(); + } return command; } } @@ -709,6 +749,10 @@ static BuildApksCommand fromFlags( ? DeviceSpecParser::parsePartialDeviceSpec : DeviceSpecParser::parseDeviceSpec; + FUSE_ONLY_DEVICE_MATCHING_MODULES_FLAG + .getValue(flags) + .ifPresent(buildApksCommand::setFuseOnlyDeviceMatchingModules); + Optional> systemApkOptions = SYSTEM_APK_OPTIONS.getValue(flags); if (systemApkOptions.isPresent() && !apkBuildMode.equals(SYSTEM)) { throw InvalidCommandException.builder() @@ -734,9 +778,21 @@ static BuildApksCommand fromFlags( P7ZipCommand.defaultP7ZipCommand(p7zipPath, numThreads)); }); + if (RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getValue(flags).isPresent() + && RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getValue(flags).isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Only one of '%s' and '%s' flags can be set.", + RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName(), + RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getName()) + .build(); + } RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG .getValue(flags) .ifPresent(buildApksCommand::setRuntimeEnabledSdkBundlePaths); + RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG + .getValue(flags) + .ifPresent(buildApksCommand::setRuntimeEnabledSdkArchivePaths); APP_STORE_PACKAGE_NAME_FLAG.getValue(flags).ifPresent(buildApksCommand::setAppStorePackageName); @@ -761,12 +817,9 @@ public Path execute() { AppBundleValidator bundleValidator = AppBundleValidator.create(getExtraValidators()); bundleValidator.validateFile(bundleZip); - ImmutableMap sdkBundles = - getValidatedSdkBundlesByPackageName(closer, tempDir); AppBundle appBundle = AppBundle.buildFromZip(bundleZip); bundleValidator.validate(appBundle); - - validateSdkBundlesMatchAppBundleDependencies(appBundle, sdkBundles); + validateRuntimeEnabledSdkDependencies(closer, tempDir, appBundle); AppBundlePreprocessorManager appBundlePreprocessorManager = DaggerAppBundlePreprocessorComponent.builder().setBuildApksCommand(this).build().create(); @@ -833,6 +886,121 @@ private void validateInput() { checkFileExistsAndReadable(path); checkFileHasExtension("ASB file", path, ".asb"); }); + getRuntimeEnabledSdkArchivePaths() + .forEach( + path -> { + checkFileExistsAndReadable(path); + checkFileHasExtension("ASAR file", path, ".asar"); + }); + } + + private void validateRuntimeEnabledSdkDependencies( + Closer closer, TempDirectory tempDir, AppBundle appBundle) throws IOException { + if (!getRuntimeEnabledSdkArchivePaths().isEmpty()) { + validateSdkAsarsMatchAppBundleDependencies( + appBundle, getSdkAsarsByPackageName(closer, tempDir)); + } else { + validateSdkBundlesMatchAppBundleDependencies( + appBundle, getValidatedSdkBundlesByPackageName(closer, tempDir)); + } + } + + private ImmutableMap getSdkAsarsByPackageName( + Closer closer, TempDirectory tempDir) throws IOException { + ImmutableListMultimap.Builder sdkArchivesPerPackageNameBuilder = + ImmutableListMultimap.builder(); + + ImmutableList sdkArchivePaths = getRuntimeEnabledSdkArchivePaths().asList(); + for (int index = 0; index < sdkArchivePaths.size(); index++) { + ZipFile sdkArchiveZip = closer.register(new ZipFile(sdkArchivePaths.get(index).toFile())); + Path sdkModulesZipPath = tempDir.getPath().resolve("tmp" + index); + ZipFile sdkModulesZip = closer.register(getModulesZip(sdkArchiveZip, sdkModulesZipPath)); + SdkAsar sdkArchive = SdkAsar.buildFromZip(sdkArchiveZip, sdkModulesZip, sdkModulesZipPath); + sdkArchivesPerPackageNameBuilder.put(sdkArchive.getPackageName(), sdkArchive); + } + + ImmutableMap> sdkArchivesPerPackageName = + sdkArchivesPerPackageNameBuilder.build().asMap(); + + sdkArchivesPerPackageName + .entrySet() + .forEach( + entry -> { + // App can not depend on multiple SDK archives with the same package name. + if (entry.getValue().size() > 1) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Received multiple SDK archives with the same package name: %s.", + entry.getKey()) + .build(); + } + }); + return ImmutableMap.copyOf( + Maps.transformValues(sdkArchivesPerPackageName, Iterables::getOnlyElement)); + } + + private static void validateSdkAsarsMatchAppBundleDependencies( + AppBundle appBundle, ImmutableMap sdkArchives) { + appBundle + .getRuntimeEnabledSdkDependencies() + .keySet() + .forEach( + sdkPackageName -> { + if (!sdkArchives.containsKey(sdkPackageName)) { + throw InvalidCommandException.builder() + .withInternalMessage( + "App bundle depends on SDK '%s', but no ASAR was provided.", sdkPackageName) + .build(); + } + RuntimeEnabledSdk sdkDependencyFromAppBundle = + appBundle.getRuntimeEnabledSdkDependencies().get(sdkPackageName); + SdkAsar sdkArchive = sdkArchives.get(sdkPackageName); + if (sdkDependencyFromAppBundle.getVersionMajor() != sdkArchive.getMajorVersion()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "App bundle depends on SDK '%s' with major version '%d', but provided SDK" + + " archive has major version '%d'.", + sdkPackageName, + sdkDependencyFromAppBundle.getVersionMajor(), + sdkArchive.getMajorVersion()) + .build(); + } + if (sdkDependencyFromAppBundle.getVersionMinor() != sdkArchive.getMinorVersion()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "App bundle depends on SDK '%s' with minor version '%d', but provided SDK" + + " archive has minor version '%d'.", + sdkPackageName, + sdkDependencyFromAppBundle.getVersionMinor(), + sdkArchive.getMinorVersion()) + .build(); + } + if (!sdkDependencyFromAppBundle + .getCertificateDigest() + .equals(sdkArchive.getCertificateDigest())) { + throw InvalidCommandException.builder() + .withInternalMessage( + "App bundle depends on SDK '%s' with signing certificate '%s', but provided" + + " ASAR is for SDK with signing certificate '%s'.", + sdkPackageName, + sdkDependencyFromAppBundle.getCertificateDigest(), + sdkArchive.getCertificateDigest()) + .build(); + } + }); + + sdkArchives + .keySet() + .forEach( + sdkPackageName -> { + if (!appBundle.getRuntimeEnabledSdkDependencies().containsKey(sdkPackageName)) { + throw InvalidCommandException.builder() + .withInternalMessage( + "App bundle does not depend on SDK '%s', but SDK archive was provided.", + sdkPackageName) + .build(); + } + }); } private ImmutableMap getValidatedSdkBundlesByPackageName( @@ -1204,6 +1372,15 @@ public static CommandHelp help() { + " spec.", DEVICE_SPEC_FLAG.getName(), CONNECTED_DEVICE_FLAG.getName()) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(FUSE_ONLY_DEVICE_MATCHING_MODULES_FLAG.getName()) + .setOptional(true) + .setDescription( + "When set, conditional modules with fused attribute will be fused into the" + + " base module if they match the the device given by %s", + DEVICE_SPEC_FLAG.getName()) + .build()) .addFlag( FlagDescription.builder() .setFlagName(MODULES_FLAG.getName()) @@ -1312,9 +1489,21 @@ public static CommandHelp help() { .setExampleValue("path/to/bundle1.asb,path/to/bundle2.asb") .setOptional(true) .setDescription( - "Experimental flag for specifying paths to SDK bundles for the runtime-enabled" - + " SDKs that the App Bundle depends on, separated by commas. Each SDK" - + " bundle must have an extension .asb.") + "Paths to SDK bundles for the runtime-enabled SDKs that the App Bundle depends" + + " on, separated by commas. Each SDK bundle must have an extension .asb." + + " Can not be used together with the '%s' flag.", + RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getName()) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getName()) + .setExampleValue("path/to/asar1.asar,path/to/asar2.asar") + .setOptional(true) + .setDescription( + "Paths to SDK archives for the runtime-enabled SDKs that the App Bundle depends" + + " on, separated by commas. Each SDK archive must have an extension" + + " .asar. Can not be used together with the '%s' flag.", + RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName()) .build()) .addFlag( FlagDescription.builder() 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 f59c3040..f376e130 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 @@ -30,6 +30,7 @@ import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.commands.BuildApksCommand.SystemApkOption; import com.android.tools.build.bundletool.device.ApkMatcher; +import com.android.tools.build.bundletool.device.ModuleMatcher; import com.android.tools.build.bundletool.io.ApkSerializerManager; import com.android.tools.build.bundletool.io.ApkSetWriter; import com.android.tools.build.bundletool.io.TempDirectory; @@ -362,8 +363,20 @@ private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() { .build(); } - private static ImmutableList modulesToFuse(ImmutableList modules) { - return modules.stream().filter(BundleModule::isIncludedInFusing).collect(toImmutableList()); + private ImmutableList modulesToFuse(ImmutableList modules) { + return modules.stream() + .filter(BundleModule::isIncludedInFusing) + .filter( + module -> !command.getFuseOnlyDeviceMatchingModules() || matchModuleToDevice(module)) + .collect(toImmutableList()); + } + + private boolean matchModuleToDevice(BundleModule module) { + if (!this.deviceSpec.isPresent()) { + return false; + } + return new ModuleMatcher(this.deviceSpec.get()) + .matchesModuleTargeting(module.getModuleMetadata().getTargeting()); } private ApkOptimizations getSystemApkOptimizations() { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommand.java new file mode 100644 index 00000000..74832a4e --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommand.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.getModulesZip; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; + +import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; +import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; +import com.android.tools.build.bundletool.flags.Flag; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; +import com.android.tools.build.bundletool.transparency.CodeTransparencyCryptoUtils; +import com.android.tools.build.bundletool.validation.SdkBundleValidator; +import com.google.auto.value.AutoValue; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Optional; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +/** Command to generate an ASAR from an Android SDK Bundle. */ +@AutoValue +public abstract class BuildSdkAsarCommand { + + public static final String COMMAND_NAME = "build-sdk-asar"; + + private static final Flag SDK_BUNDLE_LOCATION_FLAG = Flag.path("sdk-bundle"); + private static final Flag APK_SIGNING_CERTIFICATE_LOCATION_FLAG = + Flag.path("apk-signing-key-certificate"); + private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); + private static final Flag OVERWRITE_OUTPUT_FLAG = Flag.booleanFlag("overwrite"); + + /** Only used as a placeholder in parsing of SDK Bundles, not represented in output. */ + private static final int PLACEHOLDER_VERSION_CODE = 1; + + abstract Path getSdkBundlePath(); + + abstract Optional getApkSigningCertificate(); + + abstract Path getOutputFile(); + + abstract boolean getOverwriteOutput(); + + /** Creates a builder for the {@link BuildSdkAsarCommand} with some default settings. */ + public static BuildSdkAsarCommand.Builder builder() { + return new AutoValue_BuildSdkAsarCommand.Builder().setOverwriteOutput(false); + } + + /** Builder for the {@link BuildSdkAsarCommand}. */ + @AutoValue.Builder + 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 APK signing certificate */ + public abstract Builder setApkSigningCertificate(X509Certificate certificate); + + /** Sets path to the output produced by the command. Must have the extension ".asar". */ + public abstract Builder setOutputFile(Path outputFile); + + /** + * Sets whether to overwrite the contents of the output file. + * + *

The default is {@code false}. If set to {@code false} and the output file is present, + * exception is thrown. + */ + public abstract Builder setOverwriteOutput(boolean overwriteOutput); + + abstract BuildSdkAsarCommand build(); + } + + public static BuildSdkAsarCommand fromFlags(ParsedFlags flags) { + Builder sdkAsarCommandBuilder = + BuildSdkAsarCommand.builder() + .setSdkBundlePath(SDK_BUNDLE_LOCATION_FLAG.getRequiredValue(flags)) + .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); + + // Optional arguments. + OVERWRITE_OUTPUT_FLAG.getValue(flags).ifPresent(sdkAsarCommandBuilder::setOverwriteOutput); + APK_SIGNING_CERTIFICATE_LOCATION_FLAG + .getValue(flags) + .map(CodeTransparencyCryptoUtils::getX509Certificate) + .ifPresent(sdkAsarCommandBuilder::setApkSigningCertificate); + + flags.checkNoUnknownFlags(); + + return sdkAsarCommandBuilder.build(); + } + + public Path execute() { + validateInput(); + + try (ZipFile bundleZip = new ZipFile(getSdkBundlePath().toFile())) { + TempDirectory tempDir = new TempDirectory(getClass().getSimpleName()); + SdkBundleValidator bundleValidator = SdkBundleValidator.create(); + bundleValidator.validateFile(bundleZip); + + Path modulesPath = tempDir.getPath().resolve(SDK_MODULES_FILE_NAME); + try (ZipFile modulesZip = getModulesZip(bundleZip, modulesPath)) { + bundleValidator.validateModulesFile(modulesZip); + SdkBundle sdkBundle = + SdkBundle.buildFromZip(bundleZip, modulesZip, PLACEHOLDER_VERSION_CODE); + bundleValidator.validate(sdkBundle); + + DaggerBuildSdkAsarManagerComponent.builder() + .setBuildSdkAsarCommand(this) + .setSdkBundle(sdkBundle) + .build() + .create() + .execute(modulesPath); + } + } catch (ZipException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage("The SDK Bundle is not a valid zip file.") + .build(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when validating the Sdk Bundle.", e); + } + + return getOutputFile(); + } + + private void validateInput() { + FilePreconditions.checkFileExistsAndReadable(getSdkBundlePath()); + FilePreconditions.checkFileHasExtension("ASB file", getSdkBundlePath(), ".asb"); + + if (!getOverwriteOutput()) { + checkFileDoesNotExist(getOutputFile()); + } + } + + public static CommandHelp help() { + return CommandHelp.builder() + .setCommandName(COMMAND_NAME) + .setCommandDescription( + CommandDescription.builder() + .setShortDescription("Generates an ASAR from an Android SDK Bundle.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(SDK_BUNDLE_LOCATION_FLAG.getName()) + .setExampleValue("path/to/SDKbundle.asb") + .setDescription("Path to SDK bundle. Must have the extension '.asb'.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(APK_SIGNING_CERTIFICATE_LOCATION_FLAG.getName()) + .setExampleValue("path/to/certificate.crt") + .setDescription("Path to SDK APK signing certificate.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OUTPUT_FILE_FLAG.getName()) + .setExampleValue("output.asar") + .setDescription("Path to where the ASAR should be created.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OVERWRITE_OUTPUT_FLAG.getName()) + .setOptional(true) + .setDescription("If set, any previous existing output will be overwritten.") + .build()) + .build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManager.java new file mode 100644 index 00000000..bbc638b7 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManager.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.transparency.CodeTransparencyCryptoUtils.getCertificateFingerprint; + +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +import com.android.tools.build.bundletool.io.SdkAsarSerializer; +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.SdkAsar; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import javax.inject.Inject; +import org.w3c.dom.Document; + +/** Executes the "build-sdk-asar" command. */ +public class BuildSdkAsarManager { + private final BuildSdkAsarCommand command; + private final SdkBundle sdkBundle; + + @Inject + BuildSdkAsarManager(BuildSdkAsarCommand command, SdkBundle sdkBundle) { + this.command = command; + this.sdkBundle = sdkBundle; + } + + void execute(Path extractedModulesFilePath) throws IOException { + if (command.getOverwriteOutput() && Files.exists(command.getOutputFile())) { + MoreFiles.deleteRecursively(command.getOutputFile(), RecursiveDeleteOption.ALLOW_INSECURE); + } + + SdkAsarSerializer.writeToDisk( + generateSdkAsar(extractedModulesFilePath), command.getOutputFile()); + } + + private SdkAsar generateSdkAsar(Path extractedModulesFilePath) { + SdkModulesConfig sdkModulesConfig = sdkBundle.getSdkModulesConfig(); + AndroidManifest manifest = sdkBundle.getModule().getAndroidManifest(); + Document manifestDoc = XmlProtoToXmlConverter.convert(manifest.getManifestRoot()); + + SdkMetadata.Builder metadata = + SdkMetadata.newBuilder() + .setPackageName(sdkModulesConfig.getSdkPackageName()) + .setSdkVersion(sdkModulesConfig.getSdkVersion()); + + command + .getApkSigningCertificate() + .map(BuildSdkAsarManager::getFormattedCertificateDigest) + .ifPresent(metadata::setCertificateDigest); + + + return SdkAsar.builder() + .setManifest(manifestDoc) + .setModule(sdkBundle.getModule()) + .setSdkModulesConfig(sdkModulesConfig) + .setModulesFile(extractedModulesFilePath.toFile()) + .setSdkMetadata(metadata.build()) + .build(); + } + + private static String getFormattedCertificateDigest(X509Certificate certificate) { + return getCertificateFingerprint(certificate).replace(' ', ':'); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerComponent.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerComponent.java new file mode 100644 index 00000000..fa1129b4 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerComponent.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import com.android.tools.build.bundletool.model.SdkBundle; +import dagger.BindsInstance; +import dagger.Component; + +/** Dagger component to create a {@link BuildSdkAsarManager}. */ +@Component(modules = {BuildSdkAsarModule.class}) +public interface BuildSdkAsarManagerComponent { + BuildSdkAsarManager create(); + + /** Builder for the {@link BuildSdkAsarManagerComponent}. */ + @Component.Builder + interface Builder { + BuildSdkAsarManagerComponent build(); + + @BindsInstance + Builder setBuildSdkAsarCommand(BuildSdkAsarCommand command); + + @BindsInstance + Builder setSdkBundle(SdkBundle sdkBundle); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarModule.java new file mode 100644 index 00000000..f84f02ee --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkAsarModule.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import dagger.Module; + +/** Dagger module for the build-sdk-asar command. */ +@Module +public abstract class BuildSdkAsarModule {} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommand.java index 3f29eab8..d370b3ee 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommand.java @@ -32,7 +32,7 @@ import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; import com.android.tools.build.bundletool.flags.Flag; import com.android.tools.build.bundletool.flags.ParsedFlags; -import com.android.tools.build.bundletool.io.SdkBundleSerializer; +import com.android.tools.build.bundletool.io.ZipFlingerBundleSerializer; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.SdkBundle; @@ -243,7 +243,7 @@ public void execute() { Files.deleteIfExists(getOutputPath()); } - new SdkBundleSerializer().writeToDisk(sdkBundle, getOutputPath()); + new ZipFlingerBundleSerializer().serializeSdkBundle(sdkBundle, getOutputPath()); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java b/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java index 83fe8de1..8d803860 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DeviceAnalyzer.java @@ -53,8 +53,12 @@ public DeviceSpec getDeviceSpec(Optional deviceId) { Device device = getAndValidateDevice(deviceId); // device.getVersion().getApiLevel() returns 1 in case of failure. - int deviceSdkVersion = device.getVersion().getApiLevel(); - checkState(deviceSdkVersion > 1, "Error retrieving device SDK version. Please try again."); + checkState( + device.getVersion().getApiLevel() > 1, + "Error retrieving device SDK version. Please try again."); + // We want to consider device's feature level instead of API level so that targeting matching + // is done properly on preview builds. + int deviceSdkVersion = device.getVersion().getFeatureLevel(); String codename = device.getVersion().getCodename(); int deviceDensity = device.getDensity(); checkState(deviceDensity > 0, "Error retrieving device density. Please try again."); diff --git a/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java index a3aab1c7..7a923e4a 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java @@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.MoreCollectors.toOptional; +import static java.util.stream.Collectors.partitioningBy; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.BuildApksResult; @@ -25,6 +26,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.VariantTargeting; import com.google.common.collect.ImmutableList; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; @@ -75,10 +77,29 @@ public VariantMatcher(DeviceSpec deviceSpec, boolean matchInstant) { * can match a full device-spec (generated from device-spec command). */ public ImmutableList getAllMatchingVariants(BuildApksResult buildApksResult) { + // Separate SDK runtime and non-sdk runtime variants. + Map> partitionedVariants = + buildApksResult.getVariantList().stream() + .collect( + partitioningBy( + variant -> + variant.getTargeting().getSdkRuntimeTargeting().getRequiresSdkRuntime(), + toImmutableList())); + + // Currently, DeviceSpec does not specify whether it has SDK runtime support or not. Until it + // does, VariantMatcher will assume that all devices have it and try to match variants with SDK + // runtime targeting. If no SDK Runtime variant matches the device, we fall back to non-sdk + // runtime variants. + ImmutableList matchingSdkRuntimeVariants = + getAllMatchingVariants(partitionedVariants.get(true)); + return matchingSdkRuntimeVariants.isEmpty() + ? getAllMatchingVariants(partitionedVariants.get(false)) + : matchingSdkRuntimeVariants; + } + + private ImmutableList getAllMatchingVariants(ImmutableList variants) { Supplier> variantsToMatch = - () -> - buildApksResult.getVariantList().stream() - .filter(variant -> isVariantInstant(variant) == matchInstant); + () -> variants.stream().filter(variant -> isVariantInstant(variant) == matchInstant); // Check if the device is compatible with the variants. variantsToMatch.get().forEach(this::checkCompatibleWithVariant); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java index 9e7dd177..c609347b 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ModuleEntriesPacker.java @@ -16,7 +16,7 @@ package com.android.tools.build.bundletool.io; import com.android.tools.build.bundletool.model.ModuleEntry; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.android.zipflinger.ZipMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -34,7 +34,7 @@ class ModuleEntriesPacker { private final Path outputZip; private final IdentityHashMap entryNameByModuleEntry; private final Map contentByEntryName; - private final Map assignedEntryNameByBundleLocation; + private final Map assignedEntryNameByBundleLocation; private final NameAssigner nameAssigner; @@ -57,7 +57,7 @@ ModuleEntriesPacker add(ModuleEntry entry) { return this; } String entryName = - entry.getBundleLocation().isPresent() + entry.getFileLocation().isPresent() ? assignedByBundleLocation(entry) : nameAssigner.nextName(); entryNameByModuleEntry.put(entry, entryName); @@ -84,7 +84,7 @@ public ModuleEntriesPack pack(Zipper zipper) { private String assignedByBundleLocation(ModuleEntry entry) { return assignedEntryNameByBundleLocation.computeIfAbsent( - entry.getBundleLocation().get(), (location) -> nameAssigner.nextName()); + entry.getFileLocation().get(), (location) -> nameAssigner.nextName()); } /** Helper class that allows to generate zip entry names. */ diff --git a/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java index 68829918..bbb63742 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ModuleSplitSerializer.java @@ -404,7 +404,7 @@ private boolean shouldUncompressBecauseOfLowRatio( // Copying logic from aapt2: require at least 10% gains in savings. if (moduleEntry.getPath().startsWith("res")) { - return compressedSize + compressedSize / 10 > uncompressedSize; + return uncompressedSize == 0 || (compressedSize + compressedSize / 10 > uncompressedSize); } return compressedSize >= uncompressedSize; } diff --git a/src/main/java/com/android/tools/build/bundletool/io/SdkAsarSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/SdkAsarSerializer.java new file mode 100644 index 00000000..c9d424e0 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/io/SdkAsarSerializer.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.io; + +import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; +import static com.android.tools.build.bundletool.model.SdkAsar.SDK_METADATA_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.tools.build.bundletool.model.SdkAsar; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.xml.XmlUtils; +import com.google.common.io.ByteSource; +import java.io.IOException; +import java.nio.file.Path; + +/** Serializer for SDK ASARs. */ +public class SdkAsarSerializer { + + /** Writes an ASAR file to disk at the given location. */ + public static void writeToDisk(SdkAsar asar, Path pathOnDisk) throws IOException { + ZipBuilder zipBuilder = new ZipBuilder(); + + zipBuilder.addFile( + ZipPath.create(MANIFEST_FILENAME), + ByteSource.wrap(XmlUtils.documentToString(asar.getManifest()).getBytes(UTF_8))); + zipBuilder.addFileFromDisk(ZipPath.create(SDK_MODULES_FILE_NAME), asar.getModulesFile()); + zipBuilder.addFileWithProtoContent( + ZipPath.create(SDK_METADATA_FILE_NAME), asar.getSdkMetadata()); + + zipBuilder.writeTo(pathOnDisk); + } + + private SdkAsarSerializer() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerAppBundleSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerBundleSerializer.java similarity index 52% rename from src/main/java/com/android/tools/build/bundletool/io/ZipFlingerAppBundleSerializer.java rename to src/main/java/com/android/tools/build/bundletool/io/ZipFlingerBundleSerializer.java index 4f319c28..4b12da61 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerAppBundleSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipFlingerBundleSerializer.java @@ -18,6 +18,10 @@ import static com.android.tools.build.bundletool.model.AppBundle.BUNDLE_CONFIG_FILE_NAME; import static com.android.tools.build.bundletool.model.AppBundle.METADATA_DIRECTORY; +import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_BUNDLE_CONFIG_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_CONFIG_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap; @@ -25,13 +29,17 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; import com.android.tools.build.bundletool.model.ModuleEntry; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; +import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.android.zipflinger.BytesSource; import com.android.zipflinger.Source; +import com.android.zipflinger.Sources; import com.android.zipflinger.ZipArchive; import com.android.zipflinger.ZipSource; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.io.ByteSource; import com.google.protobuf.MessageLite; @@ -39,18 +47,19 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; /** * Serializer of Bundle instances onto disk that sources unmodified files from on-disk bundles if * possible. */ -public final class ZipFlingerAppBundleSerializer { +public final class ZipFlingerBundleSerializer { /** Medium compression, see {@link java.util.zip.Deflater#Deflater(int)}. */ private static final int DEFAULT_COMPRESSION_LEVEL = 6; /** Writes the App Bundle on disk at the given location. */ - public void writeToDisk(AppBundle bundle, Path destBundlePath) throws IOException { + public void serializeAppBundle(AppBundle bundle, Path destBundlePath) throws IOException { try (ZipArchive zipArchive = new ZipArchive(destBundlePath)) { zipArchive.add( protoToSource( @@ -70,7 +79,8 @@ public void writeToDisk(AppBundle bundle, Path destBundlePath) throws IOExceptio } } - addEntriesFromSourceBundles(zipArchive, getUnmodifiedModuleEntries(bundle)); + addEntriesFromSourceBundles( + zipArchive, getUnmodifiedModuleEntries(bundle), /* compress= */ false); addNewEntries(zipArchive, getNewOrModifiedModuleEntries(bundle)); // Special module files are not represented as module entries (above). @@ -113,56 +123,164 @@ public void writeToDisk(AppBundle bundle, Path destBundlePath) throws IOExceptio } } + /** Writes the SDK Bundle on disk at the given location. */ + public void serializeSdkBundle(SdkBundle sdkBundle, Path pathOnDisk) throws IOException { + try (TempDirectory tempDir = new TempDirectory(getClass().getSimpleName()); + ZipArchive zipArchive = new ZipArchive(pathOnDisk)) { + // SdkBundleConfig.pb + zipArchive.add( + protoToSource( + ZipPath.create(SDK_BUNDLE_CONFIG_FILE_NAME), + sdkBundle.getSdkBundleConfig(), + DEFAULT_COMPRESSION_LEVEL)); + + // BUNDLE-METADATA + for (Entry metadataEntry : + sdkBundle.getBundleMetadata().getFileContentMap().entrySet()) { + zipArchive.add( + new BytesSource( + metadataEntry.getValue().read(), + METADATA_DIRECTORY.resolve(metadataEntry.getKey()).toString(), + DEFAULT_COMPRESSION_LEVEL)); + } + + // Modules + Path modulesPath = tempDir.getPath().resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + writeTempModulesFile(sdkBundle, modulesPath); + zipArchive.add(Sources.from(modulesPath, SDK_MODULES_FILE_NAME, DEFAULT_COMPRESSION_LEVEL)); + } + } + + private void writeTempModulesFile(SdkBundle sdkBundle, Path modulesPath) throws IOException { + try (ZipArchive modulesArchive = new ZipArchive(modulesPath)) { + + // SdkModulesConfig.pb + modulesArchive.add( + protoToSource( + ZipPath.create(SDK_MODULES_CONFIG_FILE_NAME), + sdkBundle.getSdkModulesConfig(), + DEFAULT_COMPRESSION_LEVEL)); + + // Base module (the only module in an ASB) + BundleModule module = sdkBundle.getModule(); + ZipPath moduleDir = ZipPath.create(module.getName().toString()); + + addEntriesFromSourceBundles( + modulesArchive, getUnmodifiedModuleEntries(sdkBundle), /* compress= */ true); + addNewEntries(modulesArchive, getNewOrModifiedModuleEntries(sdkBundle)); + + // Special module files are not represented as module entries (above). + modulesArchive.add( + protoToSource( + moduleDir.resolve(SpecialModuleEntry.ANDROID_MANIFEST.getPath()), + module.getAndroidManifest().getManifestRoot().getProto(), + DEFAULT_COMPRESSION_LEVEL)); + + if (module.getAssetsConfig().isPresent()) { + modulesArchive.add( + protoToSource( + moduleDir.resolve(SpecialModuleEntry.ASSETS_TABLE.getPath()), + module.getAssetsConfig().get(), + DEFAULT_COMPRESSION_LEVEL)); + } + if (module.getNativeConfig().isPresent()) { + modulesArchive.add( + protoToSource( + moduleDir.resolve(SpecialModuleEntry.NATIVE_LIBS_TABLE.getPath()), + module.getNativeConfig().get(), + DEFAULT_COMPRESSION_LEVEL)); + } + if (module.getResourceTable().isPresent()) { + modulesArchive.add( + protoToSource( + moduleDir.resolve(SpecialModuleEntry.RESOURCE_TABLE.getPath()), + module.getResourceTable().get(), + DEFAULT_COMPRESSION_LEVEL)); + } + } + } + /** Does not consider special entries or metadata. */ @VisibleForTesting static ImmutableListMultimap getUnmodifiedModuleEntries( AppBundle bundle) { - return bundle.getModules().values().stream() + return getUnmodifiedModuleEntries(bundle.getModules().values()); + } + + /** Does not consider special entries or metadata. */ + @VisibleForTesting + static ImmutableListMultimap getUnmodifiedModuleEntries( + SdkBundle bundle) { + return getUnmodifiedModuleEntries(ImmutableList.of(bundle.getModule())); + } + + /** Does not consider special entries or metadata. */ + static ImmutableListMultimap getUnmodifiedModuleEntries( + ImmutableCollection bundleModules) { + return bundleModules.stream() .collect( flatteningToImmutableListMultimap( module -> module, module -> module.getEntries().stream() - .filter(entry -> entry.getBundleLocation().isPresent()))); + .filter(entry -> entry.getFileLocation().isPresent()))); } /** Does not consider special entries or metadata. */ private static ImmutableListMultimap getNewOrModifiedModuleEntries( AppBundle bundle) { - return bundle.getModules().values().stream() + return getNewOrModifiedModuleEntries(bundle.getModules().values()); + } + + /** Does not consider special entries or metadata. */ + @VisibleForTesting + static ImmutableListMultimap getNewOrModifiedModuleEntries( + SdkBundle bundle) { + return getNewOrModifiedModuleEntries(ImmutableList.of(bundle.getModule())); + } + + /** Does not consider special entries or metadata. */ + static ImmutableListMultimap getNewOrModifiedModuleEntries( + ImmutableCollection bundleModules) { + return bundleModules.stream() .collect( flatteningToImmutableListMultimap( module -> module, module -> module.getEntries().stream() - .filter(entry -> !entry.getBundleLocation().isPresent()))); + .filter(entry -> !entry.getFileLocation().isPresent()))); } - /** Adds umodified entries to an archive, sourcing them from their original on-disk location. */ + /** Adds unmodified entries to an archive, sourcing them from their original on-disk location. */ private static void addEntriesFromSourceBundles( - ZipArchive archive, ImmutableListMultimap entries) + ZipArchive archive, + ImmutableListMultimap entries, + boolean compress) throws IOException { Map bundleSources = new HashMap<>(); for (Map.Entry moduleAndEntry : entries.entries()) { BundleModule module = moduleAndEntry.getKey(); ModuleEntry moduleEntry = moduleAndEntry.getValue(); - ModuleEntryBundleLocation location = - moduleEntry.getBundleLocation().orElseThrow(IllegalStateException::new); + ModuleEntryLocationInZipSource location = + moduleEntry.getFileLocation().orElseThrow(IllegalStateException::new); - ZipPath entryFullPathInSourceBundle = location.entryPathInBundle(); + ZipPath entryFullPathInSourceBundle = location.entryPathInFile(); ZipPath moduleDir = ZipPath.create(module.getName().toString()); ZipPath entryFullPathInDestBundle = moduleDir.resolve(moduleEntry.getPath()); - Path pathToBundle = location.pathToBundle(); + Path pathToBundle = location.pathToFile(); // We cannot use computeIfAbstent because new ZipSource may throw. ZipSource entrySource = bundleSources.containsKey(pathToBundle) ? bundleSources.get(pathToBundle) : new ZipSource(pathToBundle); bundleSources.putIfAbsent(pathToBundle, entrySource); + String entryPath = entryFullPathInSourceBundle.toString(); entrySource.select( - entryFullPathInSourceBundle.toString(), - /* newName= */ entryFullPathInDestBundle.toString()); + entryPath, + /* newName= */ entryFullPathInDestBundle.toString(), + getCompressionLevel(entrySource, entryPath, compress), + Source.NO_ALIGNMENT); } for (ZipSource source : bundleSources.values()) { @@ -177,7 +295,7 @@ private static void addNewEntries( for (Map.Entry moduleAndEntry : entries.entries()) { BundleModule module = moduleAndEntry.getKey(); ModuleEntry moduleEntry = moduleAndEntry.getValue(); - checkState(!moduleEntry.getBundleLocation().isPresent()); + checkState(!moduleEntry.getFileLocation().isPresent()); ZipPath moduleDir = ZipPath.create(module.getName().toString()); ZipPath destPath = moduleDir.resolve(moduleEntry.getPath()); @@ -191,4 +309,17 @@ private static Source protoToSource(ZipPath path, MessageLite proto, int compres throws IOException { return new BytesSource(proto.toByteArray(), path.toString(), compression); } + + private static int getCompressionLevel( + ZipSource entrySource, String entryPath, boolean compress) { + if (!compress) { + return ZipSource.COMPRESSION_NO_CHANGE; + } + if (entrySource.entries().get(entryPath).isCompressed()) { + // The entry is already compressed, keep current compression. + return ZipSource.COMPRESSION_NO_CHANGE; + } + // The entry is uncompressed, compress it. + return DEFAULT_COMPRESSION_LEVEL; + } } 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 b5c37d20..99707184 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 @@ -628,18 +628,6 @@ public boolean hasLocaleConfig() { .isPresent(); } - public Optional getAllowBackup() { - return getApplicationAttributeAsBoolean(ALLOW_BACKUP_RESOURCE_ID); - } - - public Optional getFullBackupOnly() { - return getApplicationAttributeAsBoolean(FULL_BACKUP_ONLY_RESOURCE_ID); - } - - public boolean hasBackupAgent() { - return hasApplicationAttributeAsString(BACKUP_AGENT_RESOURCE_ID); - } - /** * Extracts names of the fused modules. * @@ -861,19 +849,18 @@ public boolean hasSharedUserId() { .isPresent(); } - private Optional getApplicationAttributeAsBoolean(int resourceId) { + private Optional getApplicationAttribute(int resourceId) { return getManifestElement() .getOptionalChildElement(APPLICATION_ELEMENT_NAME) - .flatMap(app -> app.getAndroidAttribute(resourceId)) - .map(XmlProtoAttribute::getValueAsBoolean); + .flatMap(app -> app.getAndroidAttribute(resourceId)); } - private boolean hasApplicationAttributeAsString(int resourceId) { - return getManifestElement() - .getOptionalChildElement(APPLICATION_ELEMENT_NAME) - .flatMap(app -> app.getAndroidAttribute(resourceId)) - .map(XmlProtoAttribute::hasStringValue) - .orElse(false); + private Optional getApplicationAttributeAsBoolean(int resourceId) { + return getApplicationAttribute(resourceId).map(XmlProtoAttribute::getValueAsBoolean); + } + + public boolean hasApplicationAttribute(int resourceId) { + return getApplicationAttribute(resourceId).isPresent(); } public ImmutableList getSdkLibraryElements() { 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 7dce9bb8..0c7be2e1 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 @@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.collect.ImmutableMap; @@ -72,8 +72,8 @@ public Optional getModuleEntryForSignedTransparencyFile() { ModuleEntry.builder() .setContent(transparencySignedFileContent) // TODO(b/186621568): Set bundle path. - .setBundleLocation( - ModuleEntryBundleLocation.create( + .setFileLocation( + ModuleEntryLocationInZipSource.create( Paths.get(""), ZipPath.create("BUNDLE-METADATA") .resolve(BUNDLETOOL_NAMESPACE) diff --git a/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java b/src/main/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutator.java similarity index 55% rename from src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java rename to src/main/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutator.java index 1c67d122..cb24377d 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ClassesDexNameSanitizer.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutator.java @@ -18,23 +18,21 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.lang.Math.max; import static java.util.function.Function.identity; import static java.util.stream.Collectors.partitioningBy; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CheckReturnValue; +import java.util.Comparator; import java.util.Map; +import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Sanitizer of the name of dex files to workaround a Gradle plugin bug that creates a bundle with a - * dex file named "classes1.dex" instead of "classes2.dex". - * - *

This will be removed as soon as Gradle plugin bug is fixed. - */ -public class ClassesDexNameSanitizer { +/** Mutator of the dex entries of a module. */ +public class ClassesDexEntriesMutator { private static final Pattern CLASSES_DEX_REGEX_PATTERN = Pattern.compile("dex/classes(\\d*)\\.dex"); @@ -42,9 +40,40 @@ public class ClassesDexNameSanitizer { private static final Predicate IS_DEX_FILE = entry -> entry.getPath().toString().matches(CLASSES_DEX_REGEX_PATTERN.pattern()); + /** + * Mutator of the name of dex files to workaround a Gradle plugin bug that creates a bundle with a + * dex file named "classes1.dex" instead of "classes2.dex". + * + *

This will be removed as soon as Gradle plugin bug is fixed. + */ + public static final Function, ImmutableList> + CLASSES_DEX_NAME_SANITIZER = + (dexEntries) -> + dexEntries.stream() + .map(ClassesDexEntriesMutator::sanitizeDexEntryName) + .collect(toImmutableList()); + + /** + * Mutator that removes dex file with the highest index from the list of entries. This is used + * when converting Android SDK Bundle to Android App Bundle module, where the last dex file of ASB + * should not be included. This is because it contains the RPackage class for the SDK, which + * should instead be inherited from the app's base module. + */ + public static final Function, ImmutableList> + R_PACKAGE_DEX_ENTRY_REMOVER = + (dexEntries) -> + dexEntries.stream() + .sorted( + Comparator.comparing( + dexEntry -> getClassesIndexForDexPath(dexEntry.getPath()))) + .limit(max(dexEntries.size() - 1, 0)) + .collect(toImmutableList()); + @CheckReturnValue - public BundleModule sanitize(BundleModule module) { - if (!module.getEntry(ZipPath.create("dex/classes1.dex")).isPresent()) { + public BundleModule applyMutation( + BundleModule module, + Function, ImmutableList> mutator) { + if (!shouldApplyMutation(module, mutator)) { return module; } @@ -54,15 +83,12 @@ public BundleModule sanitize(BundleModule module) { ImmutableList dexEntries = partitionedEntries.get(true); ImmutableList nonDexEntries = partitionedEntries.get(false); - // Build the new list of entries by keeping all non-dex entries unchanged and renaming the dex + // Build the new list of entries by keeping all non-dex entries unchanged and mutating the dex // entries. ImmutableList newEntries = ImmutableList.builder() .addAll(nonDexEntries) - .addAll( - dexEntries.stream() - .map(ClassesDexNameSanitizer::sanitizeDexEntry) - .collect(toImmutableList())) + .addAll(mutator.apply(dexEntries)) .build(); return module.toBuilder() @@ -70,7 +96,7 @@ public BundleModule sanitize(BundleModule module) { .build(); } - private static ModuleEntry sanitizeDexEntry(ModuleEntry dexEntry) { + private static ModuleEntry sanitizeDexEntryName(ModuleEntry dexEntry) { ZipPath sanitizedEntryPath = incrementClassesDexNumber(dexEntry.getPath()); return dexEntry.toBuilder().setPath(sanitizedEntryPath).build(); } @@ -85,6 +111,14 @@ private static ModuleEntry sanitizeDexEntry(ModuleEntry dexEntry) { * */ private static ZipPath incrementClassesDexNumber(ZipPath entryPath) { + int num = getClassesIndexForDexPath(entryPath); + if (num == 0) { + return entryPath; // dex/classes.dex + } + return ZipPath.create("dex/classes" + (num + 1) + ".dex"); + } + + private static int getClassesIndexForDexPath(ZipPath entryPath) { String fileName = entryPath.toString(); Matcher matcher = CLASSES_DEX_REGEX_PATTERN.matcher(fileName); @@ -92,8 +126,17 @@ private static ZipPath incrementClassesDexNumber(ZipPath entryPath) { String num = matcher.group(1); if (num.isEmpty()) { - return entryPath; // dex/classes.dex + return 0; // dex/classes.dex + } + return Integer.parseInt(num); + } + + private static boolean shouldApplyMutation( + BundleModule module, + Function, ImmutableList> mutator) { + if (mutator.equals(CLASSES_DEX_NAME_SANITIZER)) { + return module.getEntry(ZipPath.create("dex/classes1.dex")).isPresent(); } - return ZipPath.create("dex/classes" + (Integer.parseInt(num) + 1) + ".dex"); + return true; } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java index dea93489..c49b9080 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleEntry.java @@ -47,13 +47,12 @@ public abstract class ModuleEntry { public abstract ZipPath getPath(); /** - * Location of the module entry in the App Bundle file, if the entry comes from an on-disk App - * Bundle. + * Location of the module entry in the on-disk file. * *

If the content of the entry was generated or modified by bundletool, then this method should * return an empty {@link Optional}. */ - public abstract Optional getBundleLocation(); + public abstract Optional getFileLocation(); /** Returns whether entry should always be left uncompressed in generated archives. */ public abstract boolean getForceUncompressed(); @@ -124,9 +123,9 @@ public static Builder builder() { public abstract static class Builder { public abstract Builder setPath(ZipPath path); - public abstract Builder setBundleLocation(Optional location); + public abstract Builder setFileLocation(Optional location); - public abstract Builder setBundleLocation(ModuleEntryBundleLocation location); + public abstract Builder setFileLocation(ModuleEntryLocationInZipSource location); public abstract Builder setForceUncompressed(boolean forcedUncompressed); @@ -136,30 +135,31 @@ public abstract static class Builder { public abstract Builder setContent(ByteSource content); public Builder setContent(Path path) { - setBundleLocation(Optional.empty()); + setFileLocation(Optional.empty()); return setContent(MoreFiles.asByteSource(path)); } public Builder setContent(File file) { - setBundleLocation(Optional.empty()); + setFileLocation(Optional.empty()); return setContent(Files.asByteSource(file)); } public abstract ModuleEntry build(); } - /** Location of a module entry in an on-disk App Bundle. */ - // TODO(b/185874979): Ideally this information should be encoded in the content of a ModuleEntry. + /** Location of a module entry in an on-disk file. */ @AutoValue - public abstract static class ModuleEntryBundleLocation { - public static ModuleEntryBundleLocation create(Path pathToBundle, ZipPath entryPathInBundle) { - return new AutoValue_ModuleEntry_ModuleEntryBundleLocation(pathToBundle, entryPathInBundle); + public abstract static class ModuleEntryLocationInZipSource { + public static ModuleEntryLocationInZipSource create( + Path pathToBundle, ZipPath entryPathInBundle) { + return new AutoValue_ModuleEntry_ModuleEntryLocationInZipSource( + pathToBundle, entryPathInBundle); } - /** File path to the on-disk bundle. */ - public abstract Path pathToBundle(); + /** File path to the on-disk file. */ + public abstract Path pathToFile(); - /** Full path inside the bundle, including module name. */ - public abstract ZipPath entryPathInBundle(); + /** Full path inside the file, including module name. */ + public abstract ZipPath entryPathInFile(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ResourceIdRemapper.java b/src/main/java/com/android/tools/build/bundletool/model/ResourceIdRemapper.java deleted file mode 100644 index 7c1cff95..00000000 --- a/src/main/java/com/android/tools/build/bundletool/model/ResourceIdRemapper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.tools.build.bundletool.model; - -import static com.google.common.base.Preconditions.checkArgument; - -import com.android.aapt.Resources.Package; -import com.android.aapt.Resources.PackageId; -import com.android.aapt.Resources.ResourceTable; -import com.google.common.collect.Iterables; - -/** Remaps resource IDs in a {@link BundleModule} with a new package ID. */ -final class ResourceIdRemapper { - - /** - * Updates package IDs in the resource table and XML resources of the given {@link BundleModule} - * with the given {@code newResourcesPackageId}. - */ - static BundleModule remapResourceIds(BundleModule module, int newResourcesPackageId) { - return remapInResourceTable(module, newResourcesPackageId); - } - - private static BundleModule remapInResourceTable(BundleModule module, int newResourcesPackageId) { - if (!module.getResourceTable().isPresent()) { - return module; - } - ResourceTable resourceTable = module.getResourceTable().get(); - checkArgument( - resourceTable.getPackageCount() == 1, - "Module '%s' contains resource table with %s 'package' entries, but only 1 entry is" - + " allowed.", - module.getName().getName(), - resourceTable.getPackageCount()); - - Package remappedPackage = - Iterables.getOnlyElement(resourceTable.getPackageList()).toBuilder() - .setPackageId(PackageId.newBuilder().setId(newResourcesPackageId)) - .build(); - return module.toBuilder() - .setResourceTable( - resourceTable.toBuilder().clearPackage().addPackage(remappedPackage).build()) - .build(); - } - - private ResourceIdRemapper() {} -} diff --git a/src/main/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapper.java b/src/main/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapper.java new file mode 100644 index 00000000..36ad49fe --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapper.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.remapPackageIdInResourceId; +import static com.google.common.base.Preconditions.checkArgument; + +import com.android.aapt.Resources.Array; +import com.android.aapt.Resources.Attribute; +import com.android.aapt.Resources.CompoundValue; +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Entry; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Package; +import com.android.aapt.Resources.PackageId; +import com.android.aapt.Resources.Plural; +import com.android.aapt.Resources.Reference; +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Style; +import com.android.aapt.Resources.Styleable; +import com.android.aapt.Resources.Type; +import com.android.aapt.Resources.Value; + +/** + * Remaps resource IDs in the resource table of the given {@link BundleModule} with a new package + * ID. + */ +final class ResourceTablePackageIdRemapper { + + private final int newPackageId; + + ResourceTablePackageIdRemapper(int newPackageId) { + this.newPackageId = newPackageId; + } + + /** + * Updates resource IDs in the resource table of the given module with the {@code newPackageId}. + */ + BundleModule remap(BundleModule module) { + if (!module.getResourceTable().isPresent()) { + return module; + } + ResourceTable resourceTable = module.getResourceTable().get(); + checkArgument( + resourceTable.getPackageCount() == 1, + "Module '%s' contains resource table with %s 'package' entries, but only 1 entry is" + + " allowed.", + module.getName().getName(), + resourceTable.getPackageCount()); + + ResourceTable.Builder remappedResourceTable = resourceTable.toBuilder(); + remappedResourceTable.getPackageBuilderList().forEach(this::remapInPackage); + return module.toBuilder().setResourceTable(remappedResourceTable.build()).build(); + } + + private void remapInPackage(Package.Builder resourceTablePackage) { + resourceTablePackage + .setPackageId(PackageId.newBuilder().setId(newPackageId)) + .getTypeBuilderList() + .forEach(this::remapInType); + } + + private void remapInType(Type.Builder type) { + type.getEntryBuilderList().forEach(this::remapInEntry); + } + + private void remapInEntry(Entry.Builder entry) { + entry.getConfigValueBuilderList().forEach(this::remapInConfigValue); + } + + private void remapInConfigValue(ConfigValue.Builder configValue) { + if (!configValue.hasValue()) { + return; + } + remapInValue(configValue.getValueBuilder()); + } + + private void remapInValue(Value.Builder value) { + if (value.hasItem()) { + remapInItemValue(value); + } else { + remapInCompoundValue(value); + } + } + + private void remapInItemValue(Value.Builder value) { + if (!value.hasItem()) { + return; + } + remapInItem(value.getItemBuilder()); + } + + private void remapInItem(Item.Builder item) { + if (!item.hasRef()) { + return; + } + remapInReference(item.getRefBuilder()); + } + + private void remapInCompoundValue(Value.Builder value) { + if (!value.hasCompoundValue()) { + return; + } + remapInCompoundValue(value.getCompoundValueBuilder()); + } + + private void remapInCompoundValue(CompoundValue.Builder compoundValue) { + switch (compoundValue.getValueCase()) { + case ATTR: + remapInAttributeCompoundValue(compoundValue); + break; + case STYLE: + remapInStyleCompoundValue(compoundValue); + break; + case STYLEABLE: + remapInStylableCompoundValue(compoundValue); + break; + case ARRAY: + remapInArrayCompoundValue(compoundValue); + break; + case PLURAL: + remapInPluralCompoundValue(compoundValue); + break; + case MACRO: + case VALUE_NOT_SET: + } + } + + private void remapInAttributeCompoundValue(CompoundValue.Builder compoundValue) { + if (!compoundValue.hasAttr()) { + return; + } + remapInAttribute(compoundValue.getAttrBuilder()); + } + + private void remapInStyleCompoundValue(CompoundValue.Builder compoundValue) { + if (!compoundValue.hasStyle()) { + return; + } + remapInStyle(compoundValue.getStyleBuilder()); + } + + private void remapInStylableCompoundValue(CompoundValue.Builder compoundValue) { + if (!compoundValue.hasStyleable()) { + return; + } + remapInStyleable(compoundValue.getStyleableBuilder()); + } + + private void remapInArrayCompoundValue(CompoundValue.Builder compoundValue) { + if (!compoundValue.hasArray()) { + return; + } + remapInArray(compoundValue.getArrayBuilder()); + } + + private void remapInPluralCompoundValue(CompoundValue.Builder compoundValue) { + if (!compoundValue.hasPlural()) { + return; + } + remapInPlural(compoundValue.getPluralBuilder()); + } + + private void remapInAttribute(Attribute.Builder attribute) { + attribute.getSymbolBuilderList().forEach(this::remapInSymbol); + } + + private void remapInSymbol(Attribute.Symbol.Builder symbol) { + if (!symbol.hasName()) { + return; + } + remapInReference(symbol.getNameBuilder()); + } + + private void remapInStyle(Style.Builder style) { + if (style.hasParent()) { + remapInReference(style.getParentBuilder()); + } + style.getEntryBuilderList().forEach(this::remapInStyleEntry); + } + + private void remapInStyleable(Styleable.Builder styleable) { + styleable.getEntryBuilderList().forEach(this::remapInStyleableEntry); + } + + private void remapInArray(Array.Builder array) { + array.getElementBuilderList().forEach(this::remapInArrayElement); + } + + private void remapInPlural(Plural.Builder plural) { + plural.getEntryBuilderList().forEach(this::remapInPluralEntry); + } + + private void remapInStyleEntry(Style.Entry.Builder styleEntry) { + if (!styleEntry.hasKey()) { + return; + } + remapInReference(styleEntry.getKeyBuilder()); + } + + private void remapInStyleableEntry(Styleable.Entry.Builder styleableEntry) { + if (!styleableEntry.hasAttr()) { + return; + } + remapInReference(styleableEntry.getAttrBuilder()); + } + + private void remapInArrayElement(Array.Element.Builder element) { + if (!element.hasItem()) { + return; + } + remapInItem(element.getItemBuilder()); + } + + private void remapInPluralEntry(Plural.Entry.Builder entry) { + if (!entry.hasItem()) { + return; + } + remapInItem(entry.getItemBuilder()); + } + + private void remapInReference(Reference.Builder reference) { + reference.setId(remapPackageIdInResourceId(reference.getId(), newPackageId)); + } +} 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 new file mode 100644 index 00000000..cf23a97c --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.extractModules; +import static com.android.tools.build.bundletool.model.utils.BundleParser.readSdkModulesConfig; +import static com.android.tools.build.bundletool.model.utils.BundleParser.sanitize; + +import com.android.bundle.Config.BundleConfig.BundleType; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.utils.ZipUtils; +import com.android.tools.build.bundletool.model.version.Version; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; + +/** Represents an SDK ASAR. */ +@AutoValue +public abstract class SdkAsar { + + public static final String SDK_METADATA_FILE_NAME = "SdkMetadata.pb"; + + public static SdkAsar buildFromZip(ZipFile asar, ZipFile modulesFile, Path modulesFilePath) { + SdkModulesConfig sdkModulesConfig = readSdkModulesConfig(modulesFile); + SdkAsar.Builder sdkAsarBuilder = + builder() + .setModule( + Iterables.getOnlyElement( + sanitize( + extractModules( + modulesFile, + BundleType.REGULAR, + Version.of(sdkModulesConfig.getBundletool().getVersion()), + /* apexConfig= */ Optional.empty(), + /* nonModuleDirectories= */ ImmutableSet.of())))) + .setSdkModulesConfig(sdkModulesConfig) + .setModulesFile(modulesFilePath.toFile()) + .setSdkMetadata(readSdkMetadata(asar)); + Document document; + try { + document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + } catch (ParserConfigurationException e) { + throw new IllegalStateException(e); + } + // Setting an empty manifest, because we don't need the text manifest for generating APKs. + // APK generation is the only use case for constructing an SdkAsar from a previously generated + // ASAR zip. + return sdkAsarBuilder.setManifest(document).build(); + } + + /** Returns manifest stored in the ASAR in the text format. */ + public abstract Document getManifest(); + + public abstract BundleModule getModule(); + + public abstract SdkModulesConfig getSdkModulesConfig(); + + /** Path to the RESM archive extracted from the ASAR. */ + public abstract File getModulesFile(); + + public abstract SdkMetadata getSdkMetadata(); + + /** + * Gets the SDK package name. + * + *

Note that this is different from the package name used in the APK AndroidManifest, which is + * a combination of the SDK package name and its Android version major. + */ + public String getPackageName() { + return getSdkMetadata().getPackageName(); + } + + /** Gets the major version of the SDK. */ + public int getMajorVersion() { + return getSdkMetadata().getSdkVersion().getMajor(); + } + + /** Gets the minor version of the SDK. */ + public int getMinorVersion() { + return getSdkMetadata().getSdkVersion().getMinor(); + } + + /** + * Returns the SHA-256 hash of the runtime-enabled SDK's signing certificate, represented as a + * string of bytes in hexadecimal form, with ':' separating the bytes. + */ + public String getCertificateDigest() { + return getSdkMetadata().getCertificateDigest(); + } + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_SdkAsar.Builder(); + } + + /** Builder for SDK ASARs. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setManifest(Document manifest); + + public abstract Builder setModule(BundleModule module); + + public abstract Builder setSdkModulesConfig(SdkModulesConfig sdkModulesConfig); + + public abstract Builder setModulesFile(File modulesFile); + + public abstract Builder setSdkMetadata(SdkMetadata sdkMetadata); + + public abstract SdkAsar build(); + } + + /** Loads {@link SDK_METADATA_FILE_NAME} from zip file into {@link SdkMetadata}. */ + @SuppressWarnings("ProtoParseWithRegistry") + private static SdkMetadata readSdkMetadata(ZipFile asarFile) { + ZipEntry sdkMetadataEntry = asarFile.getEntry(SDK_METADATA_FILE_NAME); + if (sdkMetadataEntry == null) { + throw InvalidBundleException.builder() + .withUserMessage("ASAR is expected to contain '%s' file.", SDK_METADATA_FILE_NAME) + .build(); + } + try { + return SdkMetadata.parseFrom(ZipUtils.asByteSource(asarFile, sdkMetadataEntry).read()); + } catch (InvalidProtocolBufferException e) { + throw InvalidBundleException.builder() + .withCause(e) + .withUserMessage("SDK metadata file '%s' could not be parsed.", SDK_METADATA_FILE_NAME) + .build(); + } catch (IOException e) { + throw new UncheckedIOException( + String.format("Error reading file '%s'.", SDK_METADATA_FILE_NAME), e); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverter.java b/src/main/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverter.java index a29c31a2..0be41c3f 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverter.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverter.java @@ -15,6 +15,8 @@ */ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.ClassesDexEntriesMutator.R_PACKAGE_DEX_ENTRY_REMOVER; + import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; @@ -25,19 +27,48 @@ */ public final class SdkBundleModuleToAppBundleModuleConverter { + private final SdkBundle sdkBundle; + private final ResourceTablePackageIdRemapper resourceTablePackageIdRemapper; + private final XmlPackageIdRemapper xmlPackageIdRemapper; + private final ClassesDexEntriesMutator classesDexEntriesMutator; + + public SdkBundleModuleToAppBundleModuleConverter( + SdkBundle sdkBundle, RuntimeEnabledSdk dependencyConfig) { + this.sdkBundle = sdkBundle; + this.resourceTablePackageIdRemapper = + new ResourceTablePackageIdRemapper(dependencyConfig.getResourcesPackageId()); + this.xmlPackageIdRemapper = new XmlPackageIdRemapper(dependencyConfig.getResourcesPackageId()); + this.classesDexEntriesMutator = new ClassesDexEntriesMutator(); + } + /** * Returns {@link SdkBundle#getModule()}, modified so that it can be added to an Android App * Bundle as a removable install-time module. */ - public static BundleModule getAppBundleModule( - SdkBundle sdkBundle, RuntimeEnabledSdk dependencyConfig) { + public BundleModule convert() { + return convertNameTypeAndManifest( + removeRPackageDexFile( + remapResourceIdsInXmlResources( + remapResourceIdsInResourceTable(sdkBundle.getModule())))); + } + + private BundleModule remapResourceIdsInResourceTable(BundleModule module) { + return resourceTablePackageIdRemapper.remap(module); + } + + private BundleModule remapResourceIdsInXmlResources(BundleModule module) { + return xmlPackageIdRemapper.remap(module); + } + + private BundleModule removeRPackageDexFile(BundleModule module) { + return classesDexEntriesMutator.applyMutation(module, R_PACKAGE_DEX_ENTRY_REMOVER); + } + + private BundleModule convertNameTypeAndManifest(BundleModule module) { // We are using modified SDK package name as a new module name. Dots are removed because special // characters are not allowed in module names. String sdkModuleName = sdkBundle.getPackageName().replace(".", ""); - BundleModule sdkModule = - ResourceIdRemapper.remapResourceIds( - sdkBundle.getModule(), dependencyConfig.getResourcesPackageId()); - return sdkModule.toBuilder() + return module.toBuilder() .setName(BundleModuleName.create(sdkModuleName)) .setModuleType(ModuleType.SDK_DEPENDENCY_MODULE) .setAndroidManifest( @@ -51,6 +82,4 @@ public static BundleModule getAppBundleModule( .save()) .build(); } - - private SdkBundleModuleToAppBundleModuleConverter() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapper.java b/src/main/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapper.java new file mode 100644 index 00000000..1c274176 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapper.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.remapPackageIdInResourceId; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.stream.Collectors.partitioningBy; + +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.XmlAttribute; +import com.android.aapt.Resources.XmlElement; +import com.android.aapt.Resources.XmlNode; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.utils.ResourcesUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteSource; +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Map; + +/** + * Remaps resource IDs in the android manifest and XML resources of the given {@link BundleModule} + * with a new package ID. + */ +final class XmlPackageIdRemapper { + + private final int newPackageId; + + XmlPackageIdRemapper(int newPackageId) { + this.newPackageId = newPackageId; + } + + /** + * Updates resource IDs in the XML resources of the given module with the {@code newPackageId}. + */ + BundleModule remap(BundleModule module) { + if (!module.getResourceTable().isPresent()) { + return module; + } + ImmutableSet xmlResourcePaths = + ResourcesUtils.getAllProtoXmlFileReferences(module.getResourceTable().get()); + // Separate XML resource entries from all other entries. + Map> partitionedEntries = + module.getEntries().stream() + .collect( + partitioningBy( + entry -> xmlResourcePaths.contains(entry.getPath()), toImmutableSet())); + // Remap resource IDs in XML resource entries, and keep all other entries unchanged. + ImmutableList newEntries = + ImmutableList.builder() + .addAll( + partitionedEntries.get(true).stream() + .map(this::remapInModuleEntry) + .collect(toImmutableSet())) + .addAll(partitionedEntries.get(false)) + .build(); + // Remap resource IDs in AndroidManifest.xml + XmlNode newAndroidManifest = remapInXmlNode(module.getAndroidManifestProto()); + return module.toBuilder() + .setAndroidManifestProto(newAndroidManifest) + .setRawEntries(newEntries) + .build(); + } + + private ModuleEntry remapInModuleEntry(ModuleEntry moduleEntry) { + try (InputStream inputStream = moduleEntry.getContent().openStream()) { + XmlNode remappedXmlNode = remapInXmlNode(XmlNode.parseFrom(inputStream)); + return moduleEntry.toBuilder() + .setContent(ByteSource.wrap(remappedXmlNode.toByteArray())) + .build(); + } catch (InvalidProtocolBufferException e) { + throw CommandExecutionException.builder() + .withInternalMessage("Error parsing XML file '%s'.", moduleEntry.getPath()) + .withCause(e) + .build(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when parsing an XML file.", e); + } + } + + private XmlNode remapInXmlNode(XmlNode xmlNode) { + XmlNode.Builder remappedXmlNodeBuilder = xmlNode.toBuilder(); + remapInXmlNode(remappedXmlNodeBuilder); + return remappedXmlNodeBuilder.build(); + } + + private void remapInXmlNode(XmlNode.Builder xmlNode) { + if (xmlNode.hasElement()) { + remapInElement(xmlNode.getElementBuilder()); + } + } + + private void remapInElement(XmlElement.Builder xmlElement) { + xmlElement.getAttributeBuilderList().forEach(this::remapInAttribute); + xmlElement.getChildBuilderList().forEach(this::remapInXmlNode); + } + + private void remapInAttribute(XmlAttribute.Builder xmlAttribute) { + if (xmlAttribute.getResourceId() > 0) { + xmlAttribute.setResourceId( + remapPackageIdInResourceId(xmlAttribute.getResourceId(), newPackageId)); + } + if (xmlAttribute.hasCompiledItem()) { + remapInCompiledItem(xmlAttribute.getCompiledItemBuilder()); + } + } + + private void remapInCompiledItem(Item.Builder compiledItem) { + if (compiledItem.getRef().getId() > 0) { + compiledItem + .getRefBuilder() + .setId(remapPackageIdInResourceId(compiledItem.getRef().getId(), newPackageId)); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/BundleModuleParser.java b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleModuleParser.java index feb2add3..166bdb17 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/BundleModuleParser.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleModuleParser.java @@ -26,8 +26,10 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.version.Version; +import java.nio.file.Paths; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -67,6 +69,10 @@ private static BundleModule.Builder parseBundleModuleInternal( .map( zipEntry -> ModuleEntry.builder() + .setFileLocation( + ModuleEntryLocationInZipSource.create( + Paths.get(moduleZipFile.getName()), + ZipPath.create(zipEntry.getName()))) .setPath(ZipPath.create(zipEntry.getName())) .setContent(ZipUtils.asByteSource(moduleZipFile, zipEntry)) .build()) diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java index f6bf61ef..f2ec15a8 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/BundleParser.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model.utils; +import static com.android.tools.build.bundletool.model.ClassesDexEntriesMutator.CLASSES_DEX_NAME_SANITIZER; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -27,9 +28,9 @@ import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; -import com.android.tools.build.bundletool.model.ClassesDexNameSanitizer; +import com.android.tools.build.bundletool.model.ClassesDexEntriesMutator; import com.android.tools.build.bundletool.model.ModuleEntry; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.version.Version; @@ -138,8 +139,8 @@ public static ImmutableList extractModules( moduleBuilder.addEntry( ModuleEntry.builder() - .setBundleLocation( - ModuleEntryBundleLocation.create( + .setFileLocation( + ModuleEntryLocationInZipSource.create( Paths.get(bundleFile.getName()), ZipPath.create(entry.getName()))) .setPath(ZipUtils.convertBundleToModulePath(ZipPath.create(entry.getName()))) .setContent(ZipUtils.asByteSource(bundleFile, entry)) @@ -263,7 +264,12 @@ public static BundleMetadata readBundleMetadata(ZipFile bundleFile) { @CheckReturnValue public static ImmutableList sanitize(ImmutableList modules) { modules = - modules.stream().map(new ClassesDexNameSanitizer()::sanitize).collect(toImmutableList()); + modules.stream() + .map( + module -> + new ClassesDexEntriesMutator() + .applyMutation(module, CLASSES_DEX_NAME_SANITIZER)) + .collect(toImmutableList()); return modules; } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java index aa23fcd3..4755f807 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResourcesUtils.java @@ -22,6 +22,7 @@ import com.android.aapt.Resources.ConfigValue; import com.android.aapt.Resources.Entry; +import com.android.aapt.Resources.FileReference; import com.android.aapt.Resources.Package; import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.Type; @@ -173,9 +174,15 @@ public static Stream configValues(ResourceTable resourceTable) { } public static ImmutableSet getAllFileReferences(ResourceTable resourceTable) { - return configValues(resourceTable) - .filter(configValue -> configValue.getValue().getItem().hasFile()) - .map(configValue -> ZipPath.create(configValue.getValue().getItem().getFile().getPath())) + return getAllFileReferencesInternal(resourceTable) + .map(fileReference -> ZipPath.create(fileReference.getPath())) + .collect(toImmutableSet()); + } + + public static ImmutableSet getAllProtoXmlFileReferences(ResourceTable resourceTable) { + return getAllFileReferencesInternal(resourceTable) + .filter(fileReference -> fileReference.getType().equals(FileReference.Type.PROTO_XML)) + .map(fileReference -> ZipPath.create(fileReference.getPath())) .collect(toImmutableSet()); } @@ -246,6 +253,20 @@ public static DensityAlias getLowestDensity(ImmutableCollection de return densities.stream().min(comparing(DENSITY_ALIAS_TO_DPI_MAP::get)).get(); } + /** + * Returns new resource ID which is obtained from setting package ID part of {@code resourceId} to + * {@code newPackageId}. + */ + public static int remapPackageIdInResourceId(int resourceId, int newPackageId) { + return (newPackageId << 24) | (resourceId & 0xffffff); + } + + private static Stream getAllFileReferencesInternal(ResourceTable resourceTable) { + return configValues(resourceTable) + .filter(configValue -> configValue.getValue().getItem().hasFile()) + .map(configValue -> configValue.getValue().getItem().getFile()); + } + // Not meant to be instantiated. private ResourcesUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java index df3c50e0..c9ffce0e 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java @@ -201,9 +201,14 @@ public static VariantTargeting lPlusVariantTargeting() { return variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_L_API_VERSION))); } - public static VariantTargeting sdkRuntimeVariantTargeting(int sdkVersion) { - return variantTargeting(sdkVersionTargeting(sdkVersionFrom(sdkVersion))).toBuilder() + public static VariantTargeting sdkRuntimeVariantTargeting( + SdkVersion sdkVersion, ImmutableSet alternativeSdkVersions) { + return VariantTargeting.newBuilder() .setSdkRuntimeTargeting(SdkRuntimeTargeting.newBuilder().setRequiresSdkRuntime(true)) + .setSdkVersionTargeting( + SdkVersionTargeting.newBuilder() + .addValue(sdkVersion) + .addAllAlternatives(alternativeSdkVersions)) .build(); } 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 806e1670..e4ec6798 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.10.1"; + private static final String CURRENT_VERSION = "1.11.0"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGenerator.java index 6cc1eadb..217016cd 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGenerator.java @@ -16,15 +16,17 @@ package com.android.tools.build.bundletool.splitters; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkRuntimeVariantTargeting; +import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import com.android.bundle.Targeting.SdkRuntimeTargeting; +import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.SdkVersionTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.utils.Versions; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import com.google.common.collect.Streams; import java.util.stream.Stream; import javax.inject.Inject; @@ -61,19 +63,28 @@ public ImmutableSet generate( return ImmutableSet.of(); } - return Streams.concat( - sdkVersionVariantTargetings.stream() - .filter( - variantTargeting -> - variantTargeting.getSdkVersionTargeting().getValue(0).getMin().getValue() - > ANDROID_T_API_VERSION) - .map( - variantTargeting -> - variantTargeting.toBuilder() - .setSdkRuntimeTargeting( - SdkRuntimeTargeting.newBuilder().setRequiresSdkRuntime(true)) - .build()), - Stream.of(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION))) + ImmutableSet sdkVersions = + Streams.concat( + Stream.of(sdkVersionFrom(ANDROID_T_API_VERSION)), + sdkVersionVariantTargetings.stream() + .filter( + variantTargeting -> + variantTargeting + .getSdkVersionTargeting() + .getValue(0) + .getMin() + .getValue() + > ANDROID_T_API_VERSION) + .map(VariantTargeting::getSdkVersionTargeting) + .flatMap(sdkVersionTargeting -> sdkVersionTargeting.getValueList().stream())) + .collect(toImmutableSet()); + + return sdkVersions.stream() + .map( + sdkVersion -> + sdkRuntimeVariantTargeting( + sdkVersion, + Sets.difference(sdkVersions, ImmutableSet.of(sdkVersion)).immutableCopy())) .collect(toImmutableSet()); } } diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java index d9f63b42..a184669f 100644 --- a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java +++ b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java @@ -68,10 +68,10 @@ private static Stream getCodeRelatedFileEntries(BundleModule module } private static CodeRelatedFile createCodeRelatedFile(ModuleEntry moduleEntry) { - checkArgument(moduleEntry.getBundleLocation().isPresent()); + checkArgument(moduleEntry.getFileLocation().isPresent()); CodeRelatedFile.Builder codeRelatedFile = CodeRelatedFile.newBuilder() - .setPath(moduleEntry.getBundleLocation().get().entryPathInBundle().toString()); + .setPath(moduleEntry.getFileLocation().get().entryPathInFile().toString()); if (moduleEntry.getPath().startsWith(BundleModule.LIB_DIRECTORY)) { codeRelatedFile.setType(CodeRelatedFile.Type.NATIVE_LIBRARY); codeRelatedFile.setApkPath(moduleEntry.getPath().toString()); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java index 882d30d8..d6fa3232 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleZipValidator.java @@ -32,5 +32,13 @@ public void validateBundleZipEntry(ZipFile bundleFile, ZipEntry zipEntry) { zipEntry.getName()) .build(); } + if (zipEntry.getName().startsWith("/")) { + throw InvalidBundleException.builder() + .withUserMessage( + "The bundle zip file contains a zip entry starting with /, which is not " + + "allowed: '%s'.", + zipEntry.getName()) + .build(); + } } } diff --git a/src/main/proto/sdk_metadata.proto b/src/main/proto/sdk_metadata.proto new file mode 100644 index 00000000..e7d29d72 --- /dev/null +++ b/src/main/proto/sdk_metadata.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package android.bundle; + +import "sdk_modules_config.proto"; + +option java_package = "com.android.bundle"; + +// Information that uniquely identifies this runtime-enabled SDK version. +// This will be stored inside the ASAR for an SDK. +message SdkMetadata { + // Package name of the runtime-enabled SDK. + string package_name = 1; + + // Version information for the runtime-enabled SDK. + RuntimeEnabledSdkVersion sdk_version = 2; + + // SHA-256 hash of the runtime-enabled SDK's signing certificate, represented + // as a string of bytes in hexadecimal form, with ':' separating the bytes. + string certificate_digest = 3; +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java index af747983..5095c17d 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 @@ -42,7 +42,9 @@ import static com.android.tools.build.bundletool.testing.TestUtils.createDebugKeystore; import static com.android.tools.build.bundletool.testing.TestUtils.createInvalidSdkAndroidManifest; import static com.android.tools.build.bundletool.testing.TestUtils.createKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModules; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModulesWithoutManifest; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkAsarWithModules; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkBundle; import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkBundleWithModules; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; @@ -61,6 +63,7 @@ import com.android.bundle.CodeTransparencyOuterClass.CodeTransparency; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; @@ -150,6 +153,8 @@ public class BuildApksCommandTest { private static final String OLDEST_SIGNER_KEY_ALIAS = "oldest-signer-key-alias"; private static final String VALID_CERT_FINGERPRINT = "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 String VALID_CERT_FINGERPRINT2 = + "C7:96: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:0A:02:3E"; private static final String APP_STORE_PACKAGE_NAME = "my.store"; @Rule public final TemporaryFolder tmp = new TemporaryFolder(); @@ -170,7 +175,9 @@ public class BuildApksCommandTest { private Path oldestSignerPropertiesPath; private Path sdkBundlePath1; private Path sdkBundlePath2; - private Path sdkBundleModulesPath; + private Path sdkArchivePath1; + private Path sdkArchivePath2; + private Path extractedSdkBundleModulesPath; private final AdbServer fakeAdbServer = mock(AdbServer.class); private final SystemEnvironmentProvider systemEnvironmentProvider = @@ -259,12 +266,12 @@ public void setUp() throws Exception { OLDEST_SIGNER_KEYSTORE_PASSWORD, OLDEST_SIGNER_KEY_PASSWORD); - fakeAdbServer.init(Paths.get("path/to/adb")); - // Runtime-enabled-SDK-related fields. sdkBundlePath1 = tmpDir.resolve("bundle1.asb"); sdkBundlePath2 = tmpDir.resolve("bundle2.asb"); - sdkBundleModulesPath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + sdkArchivePath1 = tmpDir.resolve("archive1.asar"); + sdkArchivePath2 = tmpDir.resolve("archive2.asar"); + extractedSdkBundleModulesPath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); } @Test @@ -1271,8 +1278,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_stamp_source() throws Except } @Test - public void buildingViaFlagsAndBuilderHasSameResult_runtimeEnabledSdkBundlesSet() - throws Exception { + public void buildingViaFlagsAndBuilderHasSameResult_runtimeEnabledSdkBundlesSet() { ByteArrayOutputStream output = new ByteArrayOutputStream(); BuildApksCommand commandViaFlags = BuildApksCommand.fromFlags( @@ -1300,6 +1306,35 @@ public void buildingViaFlagsAndBuilderHasSameResult_runtimeEnabledSdkBundlesSet( assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test + public void buildingViaFlagsAndBuilderHasSameResult_runtimeEnabledSdkArchivesSet() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildApksCommand commandViaFlags = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1 + "," + sdkArchivePath2), + new PrintStream(output), + systemEnvironmentProvider, + fakeAdbServer); + + BuildApksCommand.Builder commandViaBuilder = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setRuntimeEnabledSdkArchivePaths(ImmutableSet.of(sdkArchivePath1, sdkArchivePath2)) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()); + + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + @Test public void buildingViaFlagsAndBuilderHasSameResult_customStorePackage() throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -1423,6 +1458,26 @@ public void systemApkOptions_nonSystemMode_throws() throws Exception { .contains("'system-apk-options' flag is available in system mode only."); } + @Test + public void useDeviceTargeting_noDeviceSpec_throws() throws Exception { + Throwable exception = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--fuse-only-device-matching-modules"), + fakeAdbServer)); + + assertThat(exception) + .hasMessageThat() + .contains( + "Device spec must be provided when using 'fuse-only-device-matching-modules' flag."); + } + @Test public void populateMinV3SigningApi() { ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -1695,6 +1750,46 @@ public void settingBothSigningConfigAndSigningConfigProvider_throwsError() { "Only one of SigningConfiguration or SigningConfigurationProvider should be set."); } + @Test + public void settingBothSdkBundlesAndSdkArchives_fromFlags_throws() { + Throwable e = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-bundles=" + sdkBundlePath1 + "," + sdkBundlePath2, + "--sdk-archives=" + sdkArchivePath1 + "," + sdkArchivePath2), + fakeAdbServer)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Only one of 'sdk-bundles' and 'sdk-archives' flags can be set."); + } + + @Test + public void settingBothSdkBundlesAndSdkArchives_fromBuilder_throws() { + Throwable e = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setRuntimeEnabledSdkBundlePaths( + ImmutableSet.of(sdkBundlePath1, sdkBundlePath2)) + .setRuntimeEnabledSdkArchivePaths( + ImmutableSet.of(sdkArchivePath1, sdkArchivePath2)) + .build()); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Command can only set either runtime-enabled SDK bundles or runtime-enabled SDK" + + " archives, but both were set."); + } + @Test public void sdkBundleFileDoesNotExist_throws() throws Exception { // Creating SDK bundle for sdkBundlePath1, but not for SdkBundlePath2. @@ -1722,6 +1817,33 @@ public void sdkBundleFileDoesNotExist_throws() throws Exception { assertThat(e).hasMessageThat().contains("not found"); } + @Test + public void sdkArchiveFileDoesNotExist_throws() throws Exception { + // Creating SDK archive for sdkArchivePath1, but not for sdkArchivePath2. + createSdkArchive(sdkArchivePath1, "com.test.sdk", /* majorVersion= */ 1, /* minorVersion= */ 2); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1 + "," + sdkArchivePath2), + fakeAdbServer); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e).hasMessageThat().contains("not found"); + } + @Test public void badSdkBundleFileExtension_throws() throws Exception { Path badSdkBundlePath = tmpDir.resolve("sdk_bundle.aab"); @@ -1759,6 +1881,44 @@ public void badSdkBundleFileExtension_throws() throws Exception { .contains("ASB file 'sdk_bundle.aab' is expected to have '.asb' extension."); } + @Test + public void badSdkArchiveFileExtension_throws() throws Exception { + Path badSdkArchivePath = tmpDir.resolve("sdk_archive.aab"); + createSdkArchive( + badSdkArchivePath, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); + createSdkArchive( + sdkArchivePath1, "com.test.sdk2", /* majorVersion= */ 2, /* minorVersion= */ 3); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk2") + .setVersionMajor(2) + .setVersionMinor(3) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1 + "," + badSdkArchivePath), + fakeAdbServer); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("ASAR file 'sdk_archive.aab' is expected to have '.asar' extension."); + } + @Test public void sdkBundleZipMissingModulesFile_sdkBundleZipFileValidationFails() throws Exception { createZipBuilderForSdkBundle().writeTo(sdkBundlePath1); @@ -1793,7 +1953,7 @@ public void sdkBundleZipMissingModulesFile_sdkBundleZipFileValidationFails() thr @Test public void sdkModulesZipMissingManifest_sdkModulesZipFileValidationFails() throws Exception { createZipBuilderForSdkBundleWithModules( - createZipBuilderForModulesWithoutManifest(), sdkBundleModulesPath) + createZipBuilderForModulesWithoutManifest(), extractedSdkBundleModulesPath) .writeTo(sdkBundlePath1); createAppBundleWithRuntimeEnabledSdkConfig( bundlePath, @@ -1889,6 +2049,38 @@ public void multipleSdkBundlesWithSamePackageName_validationFails() throws Excep .contains("Received multiple SDK bundles with the same package name: com.test.sdk1"); } + @Test + public void multipleSdkAsarsWithSamePackageName_validationFails() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); + createSdkArchive( + sdkArchivePath2, "com.test.sdk1", /* majorVersion= */ 2, /* minorVersion= */ 3); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1 + "," + sdkArchivePath2), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("Received multiple SDK archives with the same package name: com.test.sdk1"); + } + @Test public void missingSdkBundleInInput_throws() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); @@ -1925,6 +2117,43 @@ public void missingSdkBundleInInput_throws() throws Exception { .contains("App bundle depends on SDK 'com.test.sdk2', but no SDK bundle was provided."); } + @Test + public void missingSdkArchiveInInput_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk2") + .setVersionMajor(2) + .setVersionMinor(3) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(3)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("App bundle depends on SDK 'com.test.sdk2', but no ASAR was provided."); + } + @Test public void extraSdkBundleInInput_throws() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); @@ -1956,6 +2185,39 @@ public void extraSdkBundleInInput_throws() throws Exception { "App bundle does not depend on SDK 'com.test.sdk2', but SDK bundle was provided."); } + @Test + public void extraSdkArchiveInInput_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); + createSdkArchive( + sdkArchivePath2, "com.test.sdk2", /* majorVersion= */ 2, /* minorVersion= */ 0); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1 + "," + sdkArchivePath2), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains( + "App bundle does not depend on SDK 'com.test.sdk2', but SDK archive was provided."); + } + @Test public void sdkBundleMajorVersionMismatchWithAppBundleDependency_throws() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 3); @@ -1987,6 +2249,38 @@ public void sdkBundleMajorVersionMismatchWithAppBundleDependency_throws() throws + " bundle has major version '1'."); } + @Test + public void sdkArchiveMajorVersionMismatchWithAppBundleDependency_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 3); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(2) + .setVersionMinor(3) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "App bundle depends on SDK 'com.test.sdk1' with major version '2', but provided SDK" + + " archive has major version '1'."); + } + @Test public void sdkBundleMinorVersionMismatchWithAppBundleDependency_throws() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 0, /* minorVersion= */ 2); @@ -2012,11 +2306,81 @@ public void sdkBundleMinorVersionMismatchWithAppBundleDependency_throws() throws Exception e = assertThrows(InvalidCommandException.class, command::execute); assertThat(e) .hasMessageThat() - .contains( + .isEqualTo( "App bundle depends on SDK 'com.test.sdk1' with minor version '3', but provided SDK" + " bundle has minor version '2'."); } + @Test + public void sdkArchiveMinorVersionMismatchWithAppBundleDependency_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 0, /* minorVersion= */ 2); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMinor(3) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "App bundle depends on SDK 'com.test.sdk1' with minor version '3', but provided SDK" + + " archive has minor version '2'."); + } + + @Test + public void sdkArchiveCertificateMismatchWithAppBundleDependency_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, + "com.test.sdk1", + /* majorVersion= */ 1, + /* minorVersion= */ 2, + VALID_CERT_FINGERPRINT2); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1), + fakeAdbServer); + + Exception e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "App bundle depends on SDK 'com.test.sdk1' with signing certificate '" + + VALID_CERT_FINGERPRINT + + "', but provided ASAR is for SDK with signing certificate '" + + VALID_CERT_FINGERPRINT2 + + "'."); + } + @Test public void validateRuntimeEnabledSdkConfig_missingRequiredField_throws() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); @@ -2145,6 +2509,30 @@ private void createSdkBundle(Path path, String packageName, int majorVersion, in path); } + private void createSdkArchive(Path path, String packageName, int majorVersion, int minorVersion) + throws Exception { + createSdkArchive(path, packageName, majorVersion, minorVersion, VALID_CERT_FINGERPRINT); + } + + private void createSdkArchive( + Path path, String packageName, int majorVersion, int minorVersion, String certificateHash) + throws Exception { + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModules(), + SdkMetadata.newBuilder() + .setPackageName(packageName) + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(majorVersion) + .setMinor(minorVersion) + .setPatch(1)) + .setCertificateDigest(certificateHash) + .build(), + /* modulesPath= */ tmpDir.resolve( + packageName + majorVersion + minorVersion + "-modules.resm")) + .writeTo(path); + } + private Path createKeystorePropertiesFile( String keystorePath, String keyAlias, String keystorePassword, String keyPassword) throws IOException { 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 a1ed22fb..2e4f555b 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 @@ -643,6 +643,125 @@ public void selectsRightModules_systemApks() throws Exception { .containsExactly("assets/base.txt", "assets/fused.txt"); } + @Test + @Theory + public void fuseConditionalModulesMatchedToGivenDevice_systemApks() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/base.txt") + .setManifest(androidManifest("com.app")) + .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .addModule( + "with_fuse_no_match", + module -> + module + .addFile("assets/fused.txt") + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(true), + withDeviceGroupsCondition(ImmutableList.of("group2")), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .addModule( + "not_fused", + module -> + module + .addFile("assets/not_fused.txt") + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(false), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .addModule( + "with_fuse_and_match", + module -> + module + .addFile("assets/with_fuse_and_match.txt") + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(true), + withDeviceGroupsCondition(ImmutableList.of("group1")), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .addModule( + "without_fuse_and_match", + module -> + module + .addFile("assets/without_fuse_and_match.txt") + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(false), + withDeviceGroupsCondition(ImmutableList.of("group1")), + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID)))) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder() + .withAppBundle(appBundle) + .withOutputPath(outputFilePath) + .withApkBuildMode(SYSTEM) + .withFuseOnlyDeviceMatchingModules(true) + .withDeviceSpec( + mergeSpecs( + sdkVersion(28), abis("x86"), density(DensityAlias.MDPI), locales("en-US")) + .toBuilder() + .addDeviceGroups("group1") + .build()) + .build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + // System APKs: Only base and modules marked for fusing must be used. + assertThat(systemApkVariants(result)).hasSize(1); + ImmutableList systemApks = apkDescriptions(systemApkVariants(result)); + + assertThat(systemApks) + .containsExactly( + ApkDescription.newBuilder() + .setPath("system/system.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .addAllFusedModuleName(ImmutableList.of("base", "with_fuse_and_match"))) + .build(), + ApkDescription.newBuilder() + .setPath("splits/not_fused-master.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSplitApkMetadata( + SplitApkMetadata.newBuilder().setSplitId("not_fused").setIsMasterSplit(true)) + .build(), + ApkDescription.newBuilder() + .setPath("splits/without_fuse_and_match-master.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSplitApkMetadata( + SplitApkMetadata.newBuilder() + .setSplitId("without_fuse_and_match") + .setIsMasterSplit(true)) + .build(), + ApkDescription.newBuilder() + .setPath("splits/with_fuse_no_match-master.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSplitApkMetadata( + SplitApkMetadata.newBuilder() + .setSplitId("with_fuse_no_match") + .setIsMasterSplit(true)) + .build()); + File systemApkFile = extractFromApkSetFile(apkSetFile, "system/system.apk", outputDir); + + // Validate that the system APK contains appropriate files. + ZipFile systemApkZip = openZipFile(systemApkFile); + assertThat(filesUnderPath(systemApkZip, ZipPath.create("assets"))) + .containsExactly("assets/base.txt", "assets/with_fuse_and_match.txt"); + } + @Test @Theory public void multipleModules_systemApks_hasCorrectAdditionalLanguageSplits() throws Exception { 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 4299913d..d554bed5 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 @@ -82,7 +82,7 @@ @RunWith(JUnit4.class) public final class BuildApksPreprocessingTest { - private static final byte[] DUMMY_CONTENT = new byte[100]; + private static final byte[] TEST_CONTENT = new byte[100]; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -277,8 +277,8 @@ public void buildApksCommand_overridesAssetModuleCompression() throws Exception "base", module -> module - .addFile("dex/classes.dex", DUMMY_CONTENT) - .addFile("assets/images/image.jpg", DUMMY_CONTENT) + .addFile("dex/classes.dex", TEST_CONTENT) + .addFile("assets/images/image.jpg", TEST_CONTENT) .setManifest( androidManifest( "com.test.app", withMinSdkVersion(15), withMaxSdkVersion(27))) @@ -290,7 +290,7 @@ public void buildApksCommand_overridesAssetModuleCompression() throws Exception .setManifest( androidManifestForAssetModule( "com.test.app", withInstallTimeDelivery())) - .addFile("assets/textures/texture.etc", DUMMY_CONTENT)) + .addFile("assets/textures/texture.etc", TEST_CONTENT)) .build(); new AppBundleSerializer().writeToDisk(appBundle, bundlePath); 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 e4e12300..ec495ef6 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 @@ -88,7 +88,7 @@ public class BuildSdkApksCommandTest { @Rule public final TemporaryFolder tmp = new TemporaryFolder(); - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final BundleConfig BUNDLE_CONFIG = BundleConfigBuilder.create().build(); private static PrivateKey privateKey; @@ -306,7 +306,7 @@ public void bundleMultipleModules_throws() throws Exception { createZipBuilderForModules() .addFileWithProtoContent( ZipPath.create("feature/manifest/AndroidManifest.xml"), createSdkAndroidManifest()) - .addFileWithContent(ZipPath.create("feature/dex/classes.dex"), DUMMY_CONTENT); + .addFileWithContent(ZipPath.create("feature/dex/classes.dex"), TEST_CONTENT); createZipBuilderForSdkBundleWithModules(modules, modulesPath).writeTo(sdkBundlePath); BuildSdkApksCommand command = diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommandTest.java new file mode 100644 index 00000000..09865ac1 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarCommandTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; +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.createZipBuilderForSdkBundleWithModules; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static java.util.Arrays.stream; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.flags.ParsedFlags.UnknownFlagsException; +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.google.common.collect.ImmutableList; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BuildSdkAsarCommandTest { + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + private static final byte[] TEST_CONTENT = new byte[1]; + + private Path tmpDir; + private Path sdkBundlePath; + private Path modulesPath; + private Path outputFilePath; + private X509Certificate apkSigningKeyCertificate; + private Path apkSigningKeyCertificatePath; + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + sdkBundlePath = tmpDir.resolve("SdkBundle.asb"); + modulesPath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + outputFilePath = tmpDir.resolve("output.asar"); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(/* keysize= */ 3072); + KeyPair apkSigningKeyPair = kpg.genKeyPair(); + apkSigningKeyCertificate = + CertificateFactory.buildSelfSignedCertificate( + apkSigningKeyPair, "CN=BuildSdkAsarCommandTest_ApkSigningKey"); + apkSigningKeyCertificatePath = tmpDir.resolve("apk-signing-key-public.cert"); + Files.write(apkSigningKeyCertificatePath, apkSigningKeyCertificate.getEncoded()); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult() { + BuildSdkAsarCommand commandViaFlags = + BuildSdkAsarCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags( + "--apk-signing-key-certificate=" + apkSigningKeyCertificatePath)); + + BuildSdkAsarCommand.Builder commandViaBuilder = + BuildSdkAsarCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setOutputFile(outputFilePath) + .setApkSigningCertificate(apkSigningKeyCertificate); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void certificateDigestNotSet_usesEmptyValue() { + BuildSdkAsarCommand commandViaFlags = + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + BuildSdkAsarCommand.Builder commandViaBuilder = + BuildSdkAsarCommand.builder().setSdkBundlePath(sdkBundlePath).setOutputFile(outputFilePath); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void buildingCommandViaFlags_sdkBundlePathNotSet() { + Throwable e = + assertThrows( + RequiredFlagNotSetException.class, + () -> BuildSdkAsarCommand.fromFlags(new FlagParser().parse(""))); + assertThat(e).hasMessageThat().contains("Missing the required --sdk-bundle flag"); + } + + @Test + public void outputNotSetViaFlags_throws() { + expectMissingRequiredFlagException( + "output", + () -> + BuildSdkAsarCommand.fromFlags(new FlagParser().parse("--sdk-bundle=" + sdkBundlePath))); + } + + @Test + public void outputNotSetViaBuilder_throws() { + expectMissingRequiredBuilderPropertyException( + "outputFile", + () -> + BuildSdkAsarCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setApkSigningCertificate(apkSigningKeyCertificate) + .build()); + } + + @Test + public void bundleNotSetViaFlags_throws() { + expectMissingRequiredFlagException( + "sdk-bundle", + () -> BuildSdkAsarCommand.fromFlags(new FlagParser().parse("--output=" + outputFilePath))); + } + + @Test + public void bundleNotSetViaBuilder_throws() { + expectMissingRequiredBuilderPropertyException( + "sdkBundlePath", () -> BuildSdkAsarCommand.builder().setOutputFile(outputFilePath).build()); + } + + @Test + public void overwriteNotSetOutputFileAlreadyExists_throws() throws Exception { + new ZipBuilder().writeTo(outputFilePath); // Existing file + + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) + .writeTo(sdkBundlePath); + + BuildSdkAsarCommand command = + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e).hasMessageThat().contains("already exists"); + } + + @Test + public void unknownFlag_throws() { + UnknownFlagsException exception = + assertThrows( + UnknownFlagsException.class, + () -> + BuildSdkAsarCommand.fromFlags( + getDefaultFlagsWithAdditionalFlags("--unknownFlag=notSure"))); + + assertThat(exception) + .hasMessageThat() + .contains(String.format("Unrecognized flags: --%s", "unknownFlag")); + } + + @Test + public void missingBundleFile_throws() { + BuildSdkAsarCommand command = + BuildSdkAsarCommand.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); + BuildSdkAsarCommand command = + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "The archive doesn't seem to be an SDK Bundle, it is missing required file '" + + SDK_MODULES_FILE_NAME + + "'."); + } + + // Ensures that validations are run on the module zip file. + @Test + public void bundleMultipleModules_throws() throws Exception { + ZipBuilder modules = + createZipBuilderForModules() + .addFileWithProtoContent( + ZipPath.create("feature/manifest/AndroidManifest.xml"), createSdkAndroidManifest()) + .addFileWithContent(ZipPath.create("feature/dex/classes.dex"), TEST_CONTENT); + createZipBuilderForSdkBundleWithModules(modules, modulesPath).writeTo(sdkBundlePath); + + BuildSdkAsarCommand command = + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e).hasMessageThat().contains("SDK bundles need exactly one module"); + } + + // Ensures that validations are run on the bundle object. + @Test + public void invalidManifest_throws() throws Exception { + createZipBuilderForSdkBundleWithModules( + createZipBuilderForModulesWithInvalidManifest(), modulesPath) + .writeTo(sdkBundlePath); + BuildSdkAsarCommand command = + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()); + + 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 executeCreatesFile() throws Exception { + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) + .writeTo(sdkBundlePath); + BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()).execute(); + assertThat(Files.exists(outputFilePath)).isTrue(); + } + + @Test + public void executeReturnsOutputFile() throws Exception { + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), modulesPath) + .writeTo(sdkBundlePath); + assertThat(BuildSdkAsarCommand.fromFlags(getDefaultFlagsWithAdditionalFlags()).execute()) + .isEqualTo(outputFilePath); + } + + @Test + public void printHelpDoesNotCrash() { + BuildSdkAsarCommand.help(); + } + + private ParsedFlags getDefaultFlagsWithAdditionalFlags(String... additionalFlags) { + String[] flags = + Stream.concat(getDefaultFlagList().stream(), stream(additionalFlags)) + .toArray(String[]::new); + return new FlagParser().parse(flags); + } + + private ImmutableList getDefaultFlagList() { + return ImmutableList.of("--sdk-bundle=" + sdkBundlePath, "--output=" + outputFilePath); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerTest.java new file mode 100644 index 00000000..d4bf4ddd --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkAsarManagerTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.getModulesZip; +import static com.android.tools.build.bundletool.testing.AsarUtils.extractSdkManifest; +import static com.android.tools.build.bundletool.testing.AsarUtils.extractSdkMetadata; +import static com.android.tools.build.bundletool.testing.AsarUtils.extractSdkModuleData; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.createSdkModulesConfig; +import static com.android.tools.build.bundletool.transparency.CodeTransparencyCryptoUtils.getCertificateFingerprint; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.util.Arrays.stream; + +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.io.SdkBundleSerializer; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.android.tools.build.bundletool.testing.SdkBundleBuilder; +import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter; +import com.android.tools.build.bundletool.xml.XmlUtils; +import com.google.common.collect.ImmutableList; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.theories.Theories; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +/** Tests for {@link BuildSdkAsarManager}. */ +@RunWith(Theories.class) +public class BuildSdkAsarManagerTest { + + private static final String TEST_PACKAGE_NAME = "com.ads.foo"; + + private static X509Certificate apkSigningKeyCertificate; + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + private Path tmpDir; + private Path sdkBundlePath; + private Path modulesPath; + private Path outputFilePath; + private Path apkSigningKeyCertificatePath; + + @BeforeClass + public static void setUpClass() throws Exception { + // Creating a new key takes in average 75ms (with peaks at 200ms), so create a single key for + // all the tests. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(/* keysize= */ 3072); + KeyPair apkSigningKeyPair = kpg.genKeyPair(); + apkSigningKeyCertificate = + CertificateFactory.buildSelfSignedCertificate( + apkSigningKeyPair, "CN=BuildSdkAsarManagerTest_ApkSigningKey"); + } + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + sdkBundlePath = tmpDir.resolve("SdkBundle.asb"); + modulesPath = tmpDir.resolve("extracted-modules.resm"); + outputFilePath = tmpDir.resolve("output.asar"); + apkSigningKeyCertificatePath = tmpDir.resolve("apk-signing-key-public.cert"); + Files.write(apkSigningKeyCertificatePath, apkSigningKeyCertificate.getEncoded()); + } + + @Test + public void moduleIsCopiedToAsar() throws Exception { + SdkBundle sdkBundle = new SdkBundleBuilder().build(); + + execute(sdkBundle); + + ZipFile asarFile = new ZipFile(outputFilePath.toFile()); + byte[] moduleData = extractSdkModuleData(asarFile); + assertThat(moduleData).isEqualTo(Files.readAllBytes(modulesPath)); + } + + @Test + public void manifestIsCopiedToAsar() throws Exception { + XmlNode sdkManifest = androidManifest(TEST_PACKAGE_NAME, withMinSdkVersion(32)); + + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule(new BundleModuleBuilder("base").setManifest(sdkManifest).build()) + .build(); + + execute(sdkBundle); + + ZipFile asarFile = new ZipFile(outputFilePath.toFile()); + String asarManifest = extractSdkManifest(asarFile); + + assertThat(asarManifest) + .isEqualTo( + XmlUtils.documentToString( + XmlProtoToXmlConverter.convert(new XmlProtoNode(sdkManifest)))); + } + + @Test + public void sdkVersionInformationIsSet() throws Exception { + int major = 15; + int minor = 0; + int patch = 5; + + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setSdkModulesConfig( + createSdkModulesConfig() + .setSdkPackageName(TEST_PACKAGE_NAME) + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(major) + .setMinor(minor) + .setPatch(patch)) + .build()) + .build(); + + execute(sdkBundle); + + ZipFile asarFile = new ZipFile(outputFilePath.toFile()); + SdkMetadata asarSdkMetadata = extractSdkMetadata(asarFile); + + assertThat(asarSdkMetadata) + .isEqualTo( + SdkMetadata.newBuilder() + .setPackageName(TEST_PACKAGE_NAME) + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(major) + .setMinor(minor) + .setPatch(patch)) + .setCertificateDigest( + getCertificateFingerprint(apkSigningKeyCertificate).replace(' ', ':')) + .build()); + } + + @Test + public void certificateDigestDefaultsToEmpty() throws Exception { + int major = 15; + int minor = 0; + int patch = 5; + + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setSdkModulesConfig( + createSdkModulesConfig() + .setSdkPackageName(TEST_PACKAGE_NAME) + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(major) + .setMinor(minor) + .setPatch(patch)) + .build()) + .build(); + + execute( + sdkBundle, + BuildSdkAsarCommand.fromFlags( + new FlagParser().parse("--sdk-bundle=" + sdkBundlePath, "--output=" + outputFilePath))); + + ZipFile asarFile = new ZipFile(outputFilePath.toFile()); + SdkMetadata asarSdkMetadata = extractSdkMetadata(asarFile); + + assertThat(asarSdkMetadata) + .isEqualTo( + SdkMetadata.newBuilder() + .setPackageName(TEST_PACKAGE_NAME) + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(major) + .setMinor(minor) + .setPatch(patch)) + .build()); + } + + @Test + public void overwriteFlagOn_fileOverwritten() throws Exception { + Files.createFile(outputFilePath); + + execute(new SdkBundleBuilder().build(), createCommand("--overwrite")); + + assertThat(outputFilePath.toFile().length()).isGreaterThan(0L); + } + + private void execute(SdkBundle sdkBundle) throws Exception { + execute(sdkBundle, createCommand()); + } + + private void execute(SdkBundle sdkBundle, BuildSdkAsarCommand command) throws Exception { + new SdkBundleSerializer().writeToDisk(sdkBundle, sdkBundlePath); + + ZipFile bundleZip = new ZipFile(sdkBundlePath.toFile()); + getModulesZip(bundleZip, modulesPath); + + DaggerBuildSdkAsarManagerComponent.builder() + .setBuildSdkAsarCommand(command) + .setSdkBundle(sdkBundle) + .build() + .create() + .execute(modulesPath); + } + + private BuildSdkAsarCommand createCommand(String... additionalFlags) { + String[] flags = + Stream.concat(getDefaultFlagList().stream(), stream(additionalFlags)) + .toArray(String[]::new); + return BuildSdkAsarCommand.fromFlags(new FlagParser().parse(flags)); + } + + private ImmutableList getDefaultFlagList() { + return ImmutableList.of( + "--sdk-bundle=" + sdkBundlePath, + "--output=" + outputFilePath, + "--apk-signing-key-certificate=" + apkSigningKeyCertificatePath); + } +} 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 13f0fe4f..ae115ff4 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 @@ -111,7 +111,7 @@ @RunWith(Theories.class) public final class GetSizeCommandTest { - private static final byte[] DUMMY_BYTES = new byte[100]; + private static final byte[] TEST_BYTES = new byte[100]; @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Path tmpDir; @@ -120,7 +120,7 @@ public final class GetSizeCommandTest { @Before public void setUp() throws Exception { tmpDir = tmp.getRoot().toPath(); - compressedApkSize = GZipUtils.calculateGzipCompressedSize(ByteSource.wrap(DUMMY_BYTES)); + compressedApkSize = GZipUtils.calculateGzipCompressedSize(ByteSource.wrap(TEST_BYTES)); } @Test @@ -461,10 +461,10 @@ public void getSizeTotalInternal_singleSplitVariant() throws Exception { /* isMasterSplit= */ false))); ZipBuilder archiveBuilder = new ZipBuilder(); - archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), DUMMY_BYTES); + archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), TEST_BYTES); archiveBuilder.addFileWithContent( ZipPath.create("base-x86.apk"), - DUMMY_BYTES, + TEST_BYTES, EntryOption.UNCOMPRESSED); // APK stored uncompressed in the APKs zip. archiveBuilder.addFileWithContent(ZipPath.create("base-x86_64.apk"), new byte[10000]); archiveBuilder.addFileWithProtoContent( @@ -573,7 +573,7 @@ public void getSizeTotalInternal_withNoDimensionsAndDeviceSpec() throws Exceptio ZipPath.create("preL.apk")); ZipBuilder archiveBuilder = new ZipBuilder(); - archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), DUMMY_BYTES); + archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), TEST_BYTES); archiveBuilder.addFileWithContent(ZipPath.create("preL.apk"), new byte[10000]); archiveBuilder.addFileWithProtoContent( ZipPath.create("toc.pb"), diff --git a/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java b/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java index 6cbd3bcd..61852c96 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregatorTest.java @@ -52,10 +52,10 @@ @RunWith(JUnit4.class) public final class AssetModuleSizeAggregatorTest { - private static final long ASSET_1_MASTER_SIZE = 1 << 0; + private static final long ASSET_1_MAIN_SIZE = 1 << 0; private static final long ASSET_1_ETC2_SIZE = 1 << 1; private static final long ASSET_1_ASTC_SIZE = 1 << 2; - private static final long ASSET_2_MASTER_SIZE = 1 << 3; + private static final long ASSET_2_MAIN_SIZE = 1 << 3; private static final long ASSET_2_ETC2_SIZE = 1 << 4; private static final long ASSET_2_ASTC_SIZE = 1 << 5; private static final AssetSliceSet ASSET_MODULE_1 = @@ -63,7 +63,7 @@ public final class AssetModuleSizeAggregatorTest { "asset1", DeliveryType.INSTALL_TIME, createMasterApkDescription( - apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset1-master.apk")), + apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset1-main.apk")), createApkDescription( mergeApkTargeting( apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), @@ -81,7 +81,7 @@ public final class AssetModuleSizeAggregatorTest { "asset2", DeliveryType.INSTALL_TIME, createMasterApkDescription( - apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset2-master.apk")), + apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset2-main.apk")), createApkDescription( mergeApkTargeting( apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), @@ -96,17 +96,17 @@ public final class AssetModuleSizeAggregatorTest { /* isMasterSplit= */ false)); private static final ImmutableMap SIZE_BY_APK_PATHS = ImmutableMap.builder() - .put("asset1-master.apk", ASSET_1_MASTER_SIZE) + .put("asset1-main.apk", ASSET_1_MAIN_SIZE) .put("asset1-tcf_etc2.apk", ASSET_1_ETC2_SIZE) .put("asset1-tcf_astc.apk", ASSET_1_ASTC_SIZE) - .put("asset2-master.apk", ASSET_2_MASTER_SIZE) + .put("asset2-main.apk", ASSET_2_MAIN_SIZE) .put("asset2-tcf_etc2.apk", ASSET_2_ETC2_SIZE) .put("asset2-tcf_astc.apk", ASSET_2_ASTC_SIZE) .build(); private final GetSizeCommand.Builder getSizeCommand = GetSizeCommand.builder() - .setApksArchivePath(Paths.get("dummy.apks")) + .setApksArchivePath(Paths.get("test.apks")) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL); @Test @@ -132,9 +132,9 @@ public void getSize_singleAssetModule_noTargeting() throws Exception { "asset1", DeliveryType.INSTALL_TIME, createMasterApkDescription( - ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")))); + ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-main.apk")))); VariantTargeting variantTargeting = VariantTargeting.getDefaultInstance(); - ImmutableMap sizeByApkPaths = ImmutableMap.of("asset1-master.apk", 10L); + ImmutableMap sizeByApkPaths = ImmutableMap.of("asset1-main.apk", 10L); ConfigurationSizes configurationSizes = new AssetModuleSizeAggregator( assetModules, variantTargeting, sizeByApkPaths, getSizeCommand.build()) @@ -159,15 +159,15 @@ public void getSize_multipleAssetModules_withTargeting() throws Exception { assertThat(configurationSizes.getMinSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder().setTextureCompressionFormat("etc2").build(), - ASSET_1_MASTER_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ETC2_SIZE, + ASSET_1_MAIN_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ETC2_SIZE, SizeConfiguration.builder().setTextureCompressionFormat("astc").build(), - ASSET_1_MASTER_SIZE + ASSET_1_ASTC_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ASTC_SIZE); + ASSET_1_MAIN_SIZE + ASSET_1_ASTC_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ASTC_SIZE); assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder().setTextureCompressionFormat("etc2").build(), - ASSET_1_MASTER_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ETC2_SIZE, + ASSET_1_MAIN_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ETC2_SIZE, SizeConfiguration.builder().setTextureCompressionFormat("astc").build(), - ASSET_1_MASTER_SIZE + ASSET_1_ASTC_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ASTC_SIZE); + ASSET_1_MAIN_SIZE + ASSET_1_ASTC_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ASTC_SIZE); } @Test @@ -190,13 +190,13 @@ public void getSize_multipleAssetModules_withDeviceSpecAndVariantTargeting() thr .setTextureCompressionFormat("etc2") .setSdkVersion("21") .build(), - ASSET_1_MASTER_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ETC2_SIZE); + ASSET_1_MAIN_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ETC2_SIZE); assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder() .setTextureCompressionFormat("etc2") .setSdkVersion("21") .build(), - ASSET_1_MASTER_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MASTER_SIZE + ASSET_2_ETC2_SIZE); + ASSET_1_MAIN_SIZE + ASSET_1_ETC2_SIZE + ASSET_2_MAIN_SIZE + ASSET_2_ETC2_SIZE); } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/DeviceAnalyzerTest.java b/src/test/java/com/android/tools/build/bundletool/device/DeviceAnalyzerTest.java index 83a10eeb..b36415c6 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/DeviceAnalyzerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/DeviceAnalyzerTest.java @@ -326,7 +326,8 @@ public void extractsGlExtensions() { DeviceAnalyzer analyzer = new DeviceAnalyzer(fakeAdbServer); DeviceSpec deviceSpec = analyzer.getDeviceSpec(Optional.empty()); - assertThat(deviceSpec.getGlExtensionsList()).containsExactly("GL_EXT_extension1", "GL_EXT_extension2"); + assertThat(deviceSpec.getGlExtensionsList()) + .containsExactly("GL_EXT_extension1", "GL_EXT_extension2"); } @Test @@ -409,6 +410,28 @@ public void activityManagerFails_noProperties_defaultLocaleFallback() { assertThat(spec.getSupportedLocalesList()).containsExactly("en-US"); } + @Test + public void getDeviceSpec_deviceVersionIsPreview_sdkLevelReturnsFeatureLevel() { + int apiLevel = 21; + int featureLevel = 22; + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec( + "id1", + DeviceState.ONLINE, + mergeSpecs( + density(240), locales("en-US"), abis("x86"), sdkVersion(apiLevel, "codeName"))); + + FakeAdbServer fakeAdbServer = + new FakeAdbServer( + /* hasInitialDeviceList= */ true, /* devices= */ ImmutableList.of(fakeDevice)); + fakeAdbServer.init(Paths.get("path/to/adb")); + + DeviceAnalyzer analyzer = new DeviceAnalyzer(fakeAdbServer); + + DeviceSpec deviceSpec = analyzer.getDeviceSpec(Optional.empty()); + assertThat(deviceSpec.getSdkVersion()).isEqualTo(featureLevel); + } + private static Device createUsbEnabledDevice(String serialNumber) { return createUsbEnabledDevice(serialNumber, /* sdkVersion= */ 21, /* locale= */ "en-US"); } diff --git a/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java index 8cae194d..756dd655 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java @@ -37,11 +37,13 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.multiAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkRuntimeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.BuildApksResult; @@ -52,6 +54,7 @@ import com.android.bundle.Targeting.MultiAbiTargeting; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException; +import com.android.tools.build.bundletool.model.utils.Versions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import org.junit.Test; @@ -263,4 +266,63 @@ public void getAllMatchingVariants_apexVariants_fullDeviceSpec() { .getAllMatchingVariants(buildApksResult)) .containsExactly(x86Variant); } + + @Test + public void getMatchingVariant_sameSdkVersionDifferentSdkRuntime_sdkRuntimeReturned() { + ZipPath mainApk = ZipPath.create("main.apk"); + Variant sdkRuntimeVariant = + createVariant( + sdkRuntimeVariantTargeting(Versions.ANDROID_T_API_VERSION), + splitApkSet( + /* moduleName= */ "base", + splitApkDescription(ApkTargeting.getDefaultInstance(), mainApk))); + Variant nonSdkRuntimeVariant = + createVariant( + variantSdkTargeting(Versions.ANDROID_T_API_VERSION), + splitApkSet( + /* moduleName= */ "base", + splitApkDescription(ApkTargeting.getDefaultInstance(), mainApk))); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addAllVariant(ImmutableList.of(sdkRuntimeVariant, nonSdkRuntimeVariant)) + .build(); + DeviceSpec postLDevice = sdkVersion(Versions.ANDROID_T_API_VERSION); + VariantMatcher variantMatcher = new VariantMatcher(postLDevice); + + assertThat(variantMatcher.getMatchingVariant(buildApksResult)).hasValue(sdkRuntimeVariant); + } + + @Test + public void getMatchingVariant_differentSdkVersionSameSdkRuntime_bestSdkVersionMatched() { + ZipPath mainApk = ZipPath.create("main.apk"); + int androidU = Versions.ANDROID_T_API_VERSION + 1; + DeviceSpec androidUDevice = sdkVersion(androidU); + Variant sdkRuntimeVariantWithNonOptimalAndroidVersion = + createVariant( + sdkRuntimeVariantTargeting( + Versions.ANDROID_T_API_VERSION, + /* alternativeSdkVersions= */ ImmutableSet.of(androidU)), + splitApkSet( + /* moduleName= */ "base", + splitApkDescription(ApkTargeting.getDefaultInstance(), mainApk))); + Variant sdkRuntimeVariantWithMatchingAndroidVersion = + createVariant( + sdkRuntimeVariantTargeting( + androidU, + /* alternativeSdkVersions= */ ImmutableSet.of(Versions.ANDROID_T_API_VERSION)), + splitApkSet( + /* moduleName= */ "base", + splitApkDescription(ApkTargeting.getDefaultInstance(), mainApk))); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addAllVariant( + ImmutableList.of( + sdkRuntimeVariantWithNonOptimalAndroidVersion, + sdkRuntimeVariantWithMatchingAndroidVersion)) + .build(); + VariantMatcher variantMatcher = new VariantMatcher(androidUDevice); + + assertThat(variantMatcher.getMatchingVariant(buildApksResult)) + .hasValue(sdkRuntimeVariantWithMatchingAndroidVersion); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java b/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java index 919ff6b3..39ec614f 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregatorTest.java @@ -79,7 +79,7 @@ public class VariantTotalSizeAggregatorTest { private final GetSizeCommand.Builder getSizeCommand = GetSizeCommand.builder() - .setApksArchivePath(Paths.get("dummy.apks")) + .setApksArchivePath(Paths.get("test.apks")) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL); @Test 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 98d7465e..65772209 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 @@ -59,8 +59,8 @@ /** Tests for the {@link BundleModuleMerger} class. */ @RunWith(JUnit4.class) public class BundleModuleMergerTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; - private static final byte[] DUMMY_CONTENT_2 = new byte[2]; + private static final byte[] TEST_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT_2 = new byte[2]; private static final BundleConfig BUNDLE_CONFIG_1_0_0 = BundleConfigBuilder.create().setVersion("1.0.0").build(); private static final BundleConfig BUNDLE_CONFIG_1_8_0 = @@ -84,11 +84,11 @@ public void testMultipleModulesWithInstallTime_implicitMerging() throws Exceptio androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -108,15 +108,15 @@ public void testMultipleModulesWithInstallTime_implicitMerging_duplicateModuleEn androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail1/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail1/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail1/assets/detailsAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail2/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail2/assets/baseAssetfile.txt"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("detail2/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail2/assets/baseAssetfile.txt"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("detail2/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -136,14 +136,14 @@ public void testMultipleModulesWithInstallTime_throws_duplicateModuleEntriesWith androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail1/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail1/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail1/assets/detailsAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail2/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail2/assets/baseAssetfile.txt"), DUMMY_CONTENT_2) + .addFileWithContent(ZipPath.create("detail2/assets/baseAssetfile.txt"), TEST_CONTENT_2) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -167,11 +167,11 @@ public void testMultipleModulesWithInstallTime_noMergingIfBuiltWithOlderBundleTo androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_0_14_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -190,11 +190,11 @@ public void testMultipleModulesWithInstallTime_bundleToolVersionOverride() throw androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_0_14_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -221,20 +221,20 @@ public void testMultipleModulesWithInstallTime_bundleToolVersionOverrideIgnored( androidManifestForFeature("com.test.app.detail", withInstallTimeRemovableElement(true)); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) // merged by default .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail2/manifest/AndroidManifest.xml"), installTimeNonRemovableModuleManifest) - .addFileWithContent(ZipPath.create("detail2/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail2/assets/detailsAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail3/manifest/AndroidManifest.xml"), installTimeRemovableModuleManifest) - .addFileWithContent(ZipPath.create("detail3/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail3/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -254,11 +254,11 @@ public void testMultipleModulesWithInstallTime_notMergingAssetModules() throws E androidManifestForAssetModule("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), assetModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -281,11 +281,11 @@ public void testMultipleModulesWithInstallTime_notMergingMlModules() throws Exce androidManifestForMlModule("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -305,11 +305,11 @@ public void testMultipleModulesWithInstallTime_explicitMerging() throws Exceptio androidManifestForFeature("com.test.app.detail", withInstallTimeRemovableElement(false)); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -338,11 +338,11 @@ public void testMultipleModulesWithInstallTime_mergingOptedOut() throws Exceptio androidManifestForFeature("com.test.app.detail", withInstallTimeRemovableElement(true)); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -362,11 +362,11 @@ public void testDoNotMergeIfNotInstallTime() throws Exception { androidManifestForFeature("com.test.app.detail", withOnDemandDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), onDemandModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -387,11 +387,11 @@ public void testDoNotMergeIfConditionalModule() throws Exception { "com.test.app.detail", withMinSdkVersion(24), withFeatureCondition("android.feature")); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), conditionalModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -411,12 +411,12 @@ public void testMultipleModulesWithInstallTime_implicitMergingDexRenaming() thro androidManifestForFeature("com.test.app.detail", withInstallTimeDelivery()); createBasicZipBuilder(BUNDLE_CONFIG_1_0_0) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/baseAssetfile.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("detail/manifest/AndroidManifest.xml"), installTimeModuleManifest) - .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("detail/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/detailsAssetfile.txt"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("detail/dex/classes.dex"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java index 06011bcb..416a0153 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java @@ -110,7 +110,7 @@ @RunWith(JUnit4.class) public class ModuleSplitsToShardMergerTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final String FEATURE_MODULE_NAME = "feature"; private static final AndroidManifest DEFAULT_MANIFEST = @@ -165,14 +165,14 @@ public void mergeSingleShard_twoModulesTwoSplits() throws Exception { ModuleSplit baseModuleSplit = createModuleSplitBuilder() .setEntries( - ImmutableList.of(createModuleEntryForFile("assets/some_asset.txt", DUMMY_CONTENT))) + ImmutableList.of(createModuleEntryForFile("assets/some_asset.txt", TEST_CONTENT))) .build(); ModuleSplit featureModuleSplit = createModuleSplitBuilder() .setModuleName(BundleModuleName.create(FEATURE_MODULE_NAME)) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_other_asset.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_other_asset.txt", TEST_CONTENT))) .build(); ModuleSplit merged = @@ -193,7 +193,7 @@ public void mergeSingleShard_mergeDisjointAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -205,7 +205,7 @@ public void mergeSingleShard_mergeDisjointAssets() throws Exception { .setModuleName(BundleModuleName.create(FEATURE_MODULE_NAME)) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_other_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_other_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -234,7 +234,7 @@ public void mergeSingleShard_mergeAssetsWithIntersection() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -249,8 +249,8 @@ public void mergeSingleShard_mergeAssetsWithIntersection() throws Exception { .setModuleName(BundleModuleName.create(FEATURE_MODULE_NAME)) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT), - createModuleEntryForFile("assets/some_other_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT), + createModuleEntryForFile("assets/some_other_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -284,7 +284,7 @@ public void mergeSingleShard_throwsIfConflictingAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -298,7 +298,7 @@ public void mergeSingleShard_throwsIfConflictingAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setAssetsConfig( Assets.newBuilder() .addDirectory( @@ -585,7 +585,7 @@ public void resourceTables_areMerged() throws Exception { .setResourceTable( resourceTable(pkg(USER_PACKAGE_OFFSET, "com.test.app", drawablesType))) .setEntries( - ImmutableList.of(createModuleEntryForFile("res/drawable/image.jpg", DUMMY_CONTENT))) + ImmutableList.of(createModuleEntryForFile("res/drawable/image.jpg", TEST_CONTENT))) .build(); ModuleSplit merged = diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/ResourceTableMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/ResourceTableMergerTest.java index 3fd05d8a..4b1a6896 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ResourceTableMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ResourceTableMergerTest.java @@ -49,7 +49,7 @@ @RunWith(JUnit4.class) public class ResourceTableMergerTest { - private static final byte[] DUMMY_BYTES = new byte[1]; + private static final byte[] TEST_BYTES = new byte[1]; @Test public void packagesWithMatchingIds_merged() throws Exception { @@ -204,9 +204,8 @@ public void sourcePools_presentAndEqual_okAndPreserved() throws Exception { public void sourcePools_different_okAndSourcePoolOfFirstTablePreserved() throws Exception { ResourceTable table1 = ResourceTable.getDefaultInstance(); ResourceTable table2 = - table1 - .toBuilder() - .setSourcePool(StringPool.newBuilder().setData(ByteString.copyFrom(DUMMY_BYTES))) + table1.toBuilder() + .setSourcePool(StringPool.newBuilder().setData(ByteString.copyFrom(TEST_BYTES))) .build(); ResourceTable merged = new ResourceTableMerger().merge(table1, table2); @@ -226,7 +225,7 @@ public void sourcePools_different_sourceReferencesInSecondTableStripped() throws type(0x11, "type", entry(0x21, "entry", value("ldpi", LDPI, source1))))); ResourceTable table2 = resourceTable( - StringPool.newBuilder().setData(ByteString.copyFrom(DUMMY_BYTES)).build(), + StringPool.newBuilder().setData(ByteString.copyFrom(TEST_BYTES)).build(), pkg( 0x02, "package", @@ -251,7 +250,7 @@ public void sourcePools_different_sourceReferencesInSecondTableStripped() throws @Test public void sourcePools_equal_sourceReferencesPreserved() throws Exception { StringPool commonSourcePool = - StringPool.newBuilder().setData(ByteString.copyFrom(DUMMY_BYTES)).build(); + StringPool.newBuilder().setData(ByteString.copyFrom(TEST_BYTES)).build(); Source source1 = Source.newBuilder().setPathIdx(42).build(); Source source2 = Source.newBuilder().setPathIdx(24).build(); ResourceTable table1 = diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java index 39ebbccf..3875272a 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/SameTargetingMergerTest.java @@ -58,7 +58,7 @@ @RunWith(JUnit4.class) public class SameTargetingMergerTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final AndroidManifest DEFAULT_MANIFEST = AndroidManifest.create(androidManifest("com.test.app")); @@ -68,14 +68,14 @@ public void sameSplitTargeting_oneGroup() throws Exception { ModuleSplit moduleSplit = createModuleSplitBuilder() .setEntries( - ImmutableList.of(createModuleEntryForFile("assets/some_asset.txt", DUMMY_CONTENT))) + ImmutableList.of(createModuleEntryForFile("assets/some_asset.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .build(); ModuleSplit moduleSplit2 = createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_other_asset.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_other_asset.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .build(); @@ -100,7 +100,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/x86/liba.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/x86/liba.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.X86), abis)) .build(); @@ -108,7 +108,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/armv7-eabi/liba.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/armv7-eabi/liba.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.ARMEABI), abis)) .build(); @@ -116,7 +116,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/mips/liba.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/mips/liba.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.MIPS), abis)) .build(); @@ -124,7 +124,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/x86/libb.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/x86/libb.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.X86), abis)) .build(); @@ -132,7 +132,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/armv7-eabi/libb.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/armv7-eabi/libb.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.ARMEABI), abis)) .build(); @@ -140,7 +140,7 @@ public void sameSplitTargeting_multipleGroups() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/lib/mips/libb.so", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/lib/mips/libb.so", TEST_CONTENT))) .setMasterSplit(false) .setApkTargeting(apkAbiTargeting(ImmutableSet.of(AbiAlias.MIPS), abis)) .build(); @@ -203,7 +203,7 @@ public void mergerRejects_conflictingResourceTables() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/res/drawable/image1.jpg", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/res/drawable/image1.jpg", TEST_CONTENT))) .setResourceTable( resourceTable( pkg( @@ -215,7 +215,7 @@ public void mergerRejects_conflictingResourceTables() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("testModule/res/drawable/image2.jpg", DUMMY_CONTENT))) + createModuleEntryForFile("testModule/res/drawable/image2.jpg", TEST_CONTENT))) .setResourceTable( resourceTable( pkg( @@ -325,7 +325,7 @@ public void assetsConfigMerging_mergeDisjointAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() @@ -337,7 +337,7 @@ public void assetsConfigMerging_mergeDisjointAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_other_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_other_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() @@ -368,7 +368,7 @@ public void assetsConfigMerging_mergeAssetsWithIntersection() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() @@ -383,8 +383,8 @@ public void assetsConfigMerging_mergeAssetsWithIntersection() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT), - createModuleEntryForFile("assets/some_other_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT), + createModuleEntryForFile("assets/some_other_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() @@ -418,7 +418,7 @@ public void assetsConfigMerging_throwsIfConflictingAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() @@ -433,7 +433,7 @@ public void assetsConfigMerging_throwsIfConflictingAssets() throws Exception { createModuleSplitBuilder() .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/some_assets/file.txt", DUMMY_CONTENT))) + createModuleEntryForFile("assets/some_assets/file.txt", TEST_CONTENT))) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setAssetsConfig( Assets.newBuilder() 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 09ad5dd0..62b25367 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 @@ -16,16 +16,10 @@ package com.android.tools.build.bundletool.model; -import static com.android.tools.build.bundletool.model.AndroidManifest.ALLOW_BACKUP_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.ALLOW_BACKUP_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.BACKUP_AGENT_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.BACKUP_AGENT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.DEBUGGABLE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.DESCRIPTION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.DESCRIPTION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.DEVELOPMENT_SDK_VERSION; -import static com.android.tools.build.bundletool.model.AndroidManifest.FULL_BACKUP_ONLY_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.FULL_BACKUP_ONLY_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.HAS_CODE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.ICON_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.ICON_RESOURCE_ID; @@ -1289,46 +1283,19 @@ public void getInstantDeliveryType_installTimeElement() { } @Test - public void getAllowBackup_present() { + public void hasApplicationAttribute_present() { AndroidManifest androidManifest = - createManifestWithApplicationBooleanAttribute( - ALLOW_BACKUP_ATTRIBUTE_NAME, ALLOW_BACKUP_RESOURCE_ID, true); + createManifestWithApplicationAttribute("myAttributeName", 0x12341234, "value"); - assertThat(androidManifest.getAllowBackup()).hasValue(true); + assertThat(androidManifest.hasApplicationAttribute(0x12341234)).isTrue(); } @Test - public void getFullBackupOnly_present() { - AndroidManifest androidManifest = - createManifestWithApplicationBooleanAttribute( - FULL_BACKUP_ONLY_ATTRIBUTE_NAME, FULL_BACKUP_ONLY_RESOURCE_ID, true); - - assertThat(androidManifest.getFullBackupOnly()).hasValue(true); - } - - @Test - public void getFullBackupOnly_missing_isEmpty() { + public void hasApplicationAttribute_missing_returnsFalse() { AndroidManifest androidManifest = AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(xmlElement("application"))))); - assertThat(androidManifest.getFullBackupOnly()).isEmpty(); - } - - @Test - public void hasBackupAgent_present() { - AndroidManifest androidManifest = - createManifestWithApplicationAttribute( - BACKUP_AGENT_ATTRIBUTE_NAME, BACKUP_AGENT_RESOURCE_ID, "backup agent"); - - assertThat(androidManifest.hasBackupAgent()).isTrue(); - } - - @Test - public void hasBackupAgent_missing_returnsFalse() { - AndroidManifest androidManifest = - AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(xmlElement("application"))))); - - assertThat(androidManifest.hasBackupAgent()).isFalse(); + assertThat(androidManifest.hasApplicationAttribute(0x12341234)).isFalse(); } @Test @@ -1422,18 +1389,6 @@ private AndroidManifest createManifestWithApplicationAttribute( xmlAttribute(ANDROID_NAMESPACE_URI, name, resourceId, value)))))); } - private AndroidManifest createManifestWithApplicationBooleanAttribute( - String name, int resourceId, boolean value) { - return AndroidManifest.create( - xmlNode( - xmlElement( - "manifest", - xmlNode( - xmlElement( - "application", - xmlBooleanAttribute(ANDROID_NAMESPACE_URI, name, resourceId, value)))))); - } - private AndroidManifest createManifestWithApplicationRefIdAttribute( String name, int resourceId, int value) { return AndroidManifest.create( diff --git a/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java b/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java index 2869c248..280d15cb 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java @@ -36,7 +36,7 @@ import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.tools.build.bundletool.io.ZipBuilder; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleConfigBuilder; @@ -58,7 +58,7 @@ @RunWith(JUnit4.class) public class AppBundleTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final String PACKAGE_NAME = "com.test.app.detail"; private static final BundleConfig BUNDLE_CONFIG = BundleConfigBuilder.create().build(); public static final XmlNode MANIFEST = androidManifest(PACKAGE_NAME); @@ -76,7 +76,7 @@ public void setUp() { @Test public void testSingleModuleBundle() throws Exception { createBasicZipBuilderWithManifest() - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -90,10 +90,10 @@ public void testSingleModuleBundle() throws Exception { public void testMultipleModules() throws Exception { createBasicZipBuilder(BUNDLE_CONFIG) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/file.txt"), TEST_CONTENT) .addFileWithProtoContent(ZipPath.create("detail/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("detail/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("detail/assets/file.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -106,9 +106,9 @@ public void testMultipleModules() throws Exception { @Test public void classFilesNotAddedToModule() throws Exception { createBasicZipBuilderWithManifest() - .addFileWithContent(ZipPath.create("base/root/Foo.classes"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/root/class.txt"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/root/Foo.class"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/root/Foo.classes"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/root/class.txt"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/root/Foo.class"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -120,13 +120,13 @@ public void classFilesNotAddedToModule() throws Exception { } } - // Ensures that the ClassesDexNameSanitizer is invoked. + // Ensures that the ClassesDexEntriesMutator is invoked. @Test public void wronglyNamedDexFilesAreRenamed() throws Exception { createBasicZipBuilderWithManifest() - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -181,7 +181,7 @@ public void bundleMetadataProcessedCorrectly() throws Exception { public void bundleMetadataDirectoryNotAModule() throws Exception { createBasicZipBuilderWithManifest() .addFileWithContent( - ZipPath.create("BUNDLE-METADATA/some.namespace/metadata-file.txt"), DUMMY_CONTENT) + ZipPath.create("BUNDLE-METADATA/some.namespace/metadata-file.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -194,8 +194,8 @@ public void bundleMetadataDirectoryNotAModule() throws Exception { @Test public void metaInfDirectoryNotAModule() throws Exception { createBasicZipBuilderWithManifest() - .addFileWithContent(ZipPath.create("META-INF/GOOG.RSA"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("META-INF/MANIFEST.SF"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("META-INF/GOOG.RSA"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("META-INF/MANIFEST.SF"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -218,7 +218,7 @@ public void bundleConfigExtracted() throws Exception { @Test public void manifestRequired() throws Exception { createBasicZipBuilder(BUNDLE_CONFIG) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -233,10 +233,10 @@ public void targetedAbis_noNativeCode() throws Exception { .addModule( "base", baseModule -> - baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", TEST_CONTENT)) .addModule( "detail", - module -> module.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + module -> module.setManifest(MANIFEST).addFile("dex/classes.dex", TEST_CONTENT)) .build(); assertThat(appBundle.getTargetedAbis()).isEmpty(); @@ -251,9 +251,9 @@ public void targetedAbis_abiInAllModules() throws Exception { baseModule -> baseModule .setManifest(MANIFEST) - .addFile("dex/classes.dex", DUMMY_CONTENT) - .addFile("lib/x86_64/libfoo.so", DUMMY_CONTENT) - .addFile("lib/armeabi/libfoo.so", DUMMY_CONTENT) + .addFile("dex/classes.dex", TEST_CONTENT) + .addFile("lib/x86_64/libfoo.so", TEST_CONTENT) + .addFile("lib/armeabi/libfoo.so", TEST_CONTENT) .setNativeConfig( nativeLibraries( targetedNativeDirectory( @@ -265,9 +265,9 @@ public void targetedAbis_abiInAllModules() throws Exception { module -> module .setManifest(MANIFEST) - .addFile("dex/classes.dex", DUMMY_CONTENT) - .addFile("lib/x86_64/libbar.so", DUMMY_CONTENT) - .addFile("lib/armeabi/libbar.so", DUMMY_CONTENT) + .addFile("dex/classes.dex", TEST_CONTENT) + .addFile("lib/x86_64/libbar.so", TEST_CONTENT) + .addFile("lib/armeabi/libbar.so", TEST_CONTENT) .setNativeConfig( nativeLibraries( targetedNativeDirectory( @@ -287,15 +287,15 @@ public void targetedAbis_abiInSomeModules() throws Exception { .addModule( "base", baseModule -> - baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", TEST_CONTENT)) .addModule( "detail", module -> module .setManifest(MANIFEST) - .addFile("dex/classes.dex", DUMMY_CONTENT) - .addFile("lib/arm64-v8a/libbar.so", DUMMY_CONTENT) - .addFile("lib/x86/libbar.so", DUMMY_CONTENT) + .addFile("dex/classes.dex", TEST_CONTENT) + .addFile("lib/arm64-v8a/libbar.so", TEST_CONTENT) + .addFile("lib/x86/libbar.so", TEST_CONTENT) .setNativeConfig( nativeLibraries( targetedNativeDirectory( @@ -315,13 +315,13 @@ public void baseAndAssetModule_fromModules_areSeparated() throws Exception { .addModule( "base", baseModule -> - baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", TEST_CONTENT)) .addModule( "some_asset_module", module -> module .setManifest(ASSET_MODULE_MANIFEST) - .addFile("assets/img1.png", DUMMY_CONTENT)) + .addFile("assets/img1.png", TEST_CONTENT)) .build(); assertThat(appBundle.getFeatureModules().keySet()) @@ -334,11 +334,11 @@ public void baseAndAssetModule_fromModules_areSeparated() throws Exception { public void baseAndAssetModule_fromZipFile_areSeparated() throws Exception { createBasicZipBuilder(BUNDLE_CONFIG) .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/file.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("asset_module/manifest/AndroidManifest.xml"), ASSET_MODULE_MANIFEST) - .addFileWithContent(ZipPath.create("asset_module/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("asset_module/assets/file.txt"), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -422,7 +422,7 @@ public void bundleLocation_fromZip_areSet() throws Exception { ZipPath dexModuleEntryPath = ZipPath.create("dex/classes.dex"); createBasicZipBuilderWithManifest() - .addFileWithContent(ZipPath.create(dexZipEntry), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create(dexZipEntry), TEST_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -430,8 +430,8 @@ public void bundleLocation_fromZip_areSet() throws Exception { assertThat(appBundle.getFeatureModules().keySet()) .containsExactly(BundleModuleName.create("base")); assertThat(appBundle.getBaseModule().getEntry(dexModuleEntryPath)).isPresent(); - assertThat(appBundle.getBaseModule().getEntry(dexModuleEntryPath).get().getBundleLocation()) - .hasValue(ModuleEntryBundleLocation.create(bundleFile, ZipPath.create(dexZipEntry))); + assertThat(appBundle.getBaseModule().getEntry(dexModuleEntryPath).get().getFileLocation()) + .hasValue(ModuleEntryLocationInZipSource.create(bundleFile, ZipPath.create(dexZipEntry))); } } @@ -444,11 +444,7 @@ public void bundleLocation_notFromZip_notSet() throws Exception { .build(); assertThat(appBundle.getBaseModule().getEntry(ZipPath.create(dexFilePath))).isPresent(); assertThat( - appBundle - .getBaseModule() - .getEntry(ZipPath.create(dexFilePath)) - .get() - .getBundleLocation()) + appBundle.getBaseModule().getEntry(ZipPath.create(dexFilePath)).get().getFileLocation()) .isEmpty(); } 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 7b8e957b..e7ce5870 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 @@ -20,7 +20,7 @@ import static com.google.common.truth.Truth8.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import java.nio.charset.Charset; @@ -32,13 +32,13 @@ @RunWith(JUnit4.class) public class BundleMetadataTest { - private static final ByteSource DUMMY_DATA = ByteSource.wrap(new byte[0]); + private static final ByteSource TEST_DATA = ByteSource.wrap(new byte[0]); @Test public void addFile_plainNamespacedDirectory() throws Exception { BundleMetadata metadata = BundleMetadata.builder() - .addFile(/* namespacedDir= */ "com.namespace", /* fileName= */ "filename", DUMMY_DATA) + .addFile(/* namespacedDir= */ "com.namespace", /* fileName= */ "filename", TEST_DATA) .build(); assertThat(metadata.getFileContentMap().keySet()) @@ -52,7 +52,7 @@ public void addFile_nestedNamespacedDirectory() throws Exception { .addFile( /* namespacedDir= */ "com.namespace/dir/sub-dir", /* fileName= */ "filename", - DUMMY_DATA) + TEST_DATA) .build(); assertThat(metadata.getFileContentMap().keySet()) @@ -64,7 +64,7 @@ public void addFile_pathTooShort_throws() throws Exception { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> BundleMetadata.builder().addFile(ZipPath.create("com.namespace"), DUMMY_DATA)); + () -> BundleMetadata.builder().addFile(ZipPath.create("com.namespace"), TEST_DATA)); assertThat(exception).hasMessageThat().contains("too shallow"); } @@ -74,7 +74,7 @@ public void addFile_pathNotNamespaced_throws() throws Exception { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> BundleMetadata.builder().addFile(ZipPath.create("no_dot/filename"), DUMMY_DATA)); + () -> BundleMetadata.builder().addFile(ZipPath.create("no_dot/filename"), TEST_DATA)); assertThat(exception) .hasMessageThat() @@ -85,7 +85,7 @@ public void addFile_pathNotNamespaced_throws() throws Exception { public void getModuleEntryForSignedTransparencyFile_empty() { BundleMetadata metadata = BundleMetadata.builder() - .addFile(/* namespacedDir= */ "com.namespace", /* fileName= */ "filename", DUMMY_DATA) + .addFile(/* namespacedDir= */ "com.namespace", /* fileName= */ "filename", TEST_DATA) .build(); assertThat(metadata.getModuleEntryForSignedTransparencyFile()).isEmpty(); @@ -108,8 +108,8 @@ public void getModuleEntryForSignedTransparencyFile() { ModuleEntry.builder() .setContent(transparencyContents) // TODO(b/186621568): Fix. Bundle location is ignored in ModuleEntry.equals. - .setBundleLocation( - ModuleEntryBundleLocation.create( + .setFileLocation( + ModuleEntryLocationInZipSource.create( Paths.get(""), ZipPath.create("BUNDLE-METADATA") .resolve(BundleMetadata.BUNDLETOOL_NAMESPACE) diff --git a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java index 05650770..9d452eee 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java @@ -68,7 +68,7 @@ public class BundleModuleTest { private static final BundleConfig DEFAULT_BUNDLE_CONFIG = BundleConfigBuilder.create().build(); - private static final byte[] DUMMY_CONTENT = new byte[0]; + private static final byte[] TEST_CONTENT = new byte[0]; @Test public void missingAssetsProtoFile_returnsEmptyProto() { @@ -284,9 +284,9 @@ public void baseAlwaysIncludedInFusing() throws Exception { /** Tests that we skip directories that contain a directory that we want to find entries under. */ @Test public void entriesUnderPath_withPrefixDirectory() throws Exception { - ModuleEntry entry1 = createModuleEntryForFile("dir1/entry1", DUMMY_CONTENT); - ModuleEntry entry2 = createModuleEntryForFile("dir1/entry2", DUMMY_CONTENT); - ModuleEntry entry3 = createModuleEntryForFile("dir1longer/entry3", DUMMY_CONTENT); + ModuleEntry entry1 = createModuleEntryForFile("dir1/entry1", TEST_CONTENT); + ModuleEntry entry2 = createModuleEntryForFile("dir1/entry2", TEST_CONTENT); + ModuleEntry entry3 = createModuleEntryForFile("dir1longer/entry3", TEST_CONTENT); BundleModule bundleModule = createMinimalModuleBuilder().addEntries(Arrays.asList(entry1, entry2, entry3)).build(); @@ -297,7 +297,7 @@ public void entriesUnderPath_withPrefixDirectory() throws Exception { @Test public void getEntry_existing_found() throws Exception { - ModuleEntry entry = createModuleEntryForFile("dir/entry", DUMMY_CONTENT); + ModuleEntry entry = createModuleEntryForFile("dir/entry", TEST_CONTENT); BundleModule bundleModule = createMinimalModuleBuilder().addEntries(Arrays.asList(entry)).build(); @@ -450,8 +450,8 @@ public void renderscriptFiles_present() throws Exception { BundleModule bundleModule = createMinimalModuleBuilder() .setAndroidManifestProto(androidManifest("com.test.app")) - .addEntry(createModuleEntryForFile("dex/classes.dex", DUMMY_CONTENT)) - .addEntry(createModuleEntryForFile("res/raw/yuv2rgb.bc", DUMMY_CONTENT)) + .addEntry(createModuleEntryForFile("dex/classes.dex", TEST_CONTENT)) + .addEntry(createModuleEntryForFile("res/raw/yuv2rgb.bc", TEST_CONTENT)) .build(); assertThat(bundleModule.hasRenderscript32Bitcode()).isTrue(); @@ -462,7 +462,7 @@ public void renderscriptFiles_absent() throws Exception { BundleModule bundleModule = createMinimalModuleBuilder() .setAndroidManifestProto(androidManifest("com.test.app")) - .addEntry(createModuleEntryForFile("dex/classes.dex", DUMMY_CONTENT)) + .addEntry(createModuleEntryForFile("dex/classes.dex", TEST_CONTENT)) .build(); assertThat(bundleModule.hasRenderscript32Bitcode()).isFalse(); diff --git a/src/test/java/com/android/tools/build/bundletool/model/ClassDexNameSanitizerTest.java b/src/test/java/com/android/tools/build/bundletool/model/ClassDexNameSanitizerTest.java deleted file mode 100644 index a7c9cdba..00000000 --- a/src/test/java/com/android/tools/build/bundletool/model/ClassDexNameSanitizerTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.tools.build.bundletool.model; - -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; -import static com.android.tools.build.bundletool.testing.TestUtils.createModuleEntryForFile; -import static com.google.common.truth.Truth.assertThat; - -import com.android.bundle.Config.BundleConfig; -import com.android.tools.build.bundletool.model.version.Version; -import com.android.tools.build.bundletool.testing.BundleConfigBuilder; -import java.io.IOException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class ClassDexNameSanitizerTest { - - private static final byte[] DUMMY_CONTENT = new byte[] {0x42}; - private static final BundleConfig DEFAULT_BUNDLE_CONFIG = BundleConfigBuilder.create().build(); - - @Test - public void singleDexFile_doesNotModifyAnything() throws Exception { - BundleModule module = - createBasicModule() - .addEntry(createModuleEntryForFile("dex/classes.dex", DUMMY_CONTENT)) - .build(); - - assertThat(new ClassesDexNameSanitizer().sanitize(module)).isEqualTo(module); - } - - @Test - public void multipleDexFilesNamedProperly_doesNotModifyAnything() throws Exception { - BundleModule module = - createBasicModule() - .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) - .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) - .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) - .build(); - - assertThat(new ClassesDexNameSanitizer().sanitize(module)).isEqualTo(module); - } - - @Test - public void multipleDexNamedWithWrongDexFile_renamesCorrectly() throws Exception { - BundleModule beforeRename = - createBasicModule() - .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) - .addEntry(createModuleEntryForFile("dex/classes1.dex", new byte[] {0x2})) - .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x3})) - .build(); - - BundleModule afterRename = - createBasicModule() - .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) - .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) - .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) - .build(); - - assertThat(new ClassesDexNameSanitizer().sanitize(beforeRename)).isEqualTo(afterRename); - } - - private static BundleModule.Builder createBasicModule() throws IOException { - return BundleModule.builder() - .setName(BundleModuleName.create("module")) - .setBundleType(DEFAULT_BUNDLE_CONFIG.getType()) - .setBundletoolVersion(Version.of(DEFAULT_BUNDLE_CONFIG.getBundletool().getVersion())) - .addEntry(createModuleEntryForFile("assets/hello", new byte[] {0xD, 0xE, 0xA, 0xD})) - .addEntry(createModuleEntryForFile("assets/world", new byte[] {0xB, 0xE, 0xE, 0xF})) - .setAndroidManifestProto(androidManifest("com.test.app", withSplitId("module"))); - } -} diff --git a/src/test/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutatorTest.java new file mode 100644 index 00000000..98c2348a --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/ClassesDexEntriesMutatorTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018 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.ClassesDexEntriesMutator.CLASSES_DEX_NAME_SANITIZER; +import static com.android.tools.build.bundletool.model.ClassesDexEntriesMutator.R_PACKAGE_DEX_ENTRY_REMOVER; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; +import static com.android.tools.build.bundletool.testing.TestUtils.createModuleEntryForFile; +import static com.google.common.truth.Truth.assertThat; + +import com.android.bundle.Config.BundleConfig; +import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.testing.BundleConfigBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ClassesDexEntriesMutatorTest { + + private static final byte[] TEST_CONTENT = new byte[] {0x42}; + private static final BundleConfig DEFAULT_BUNDLE_CONFIG = BundleConfigBuilder.create().build(); + + @Test + public void classesDexNameSanitizer_singleDexFile_doesNotModifyAnything() { + BundleModule module = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", TEST_CONTENT)) + .build(); + + assertThat(new ClassesDexEntriesMutator().applyMutation(module, CLASSES_DEX_NAME_SANITIZER)) + .isEqualTo(module); + } + + @Test + public void classesDexNameSanitizer_multipleDexFilesNamedProperly_doesNotModifyAnything() { + BundleModule module = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) + .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) + .build(); + + assertThat(new ClassesDexEntriesMutator().applyMutation(module, CLASSES_DEX_NAME_SANITIZER)) + .isEqualTo(module); + } + + @Test + public void classesDexNameSanitizer_multipleDexNamedWithWrongDexFile_renamesCorrectly() { + BundleModule beforeRename = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .addEntry(createModuleEntryForFile("dex/classes1.dex", new byte[] {0x2})) + .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x3})) + .build(); + + BundleModule afterRename = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) + .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) + .build(); + + assertThat( + new ClassesDexEntriesMutator().applyMutation(beforeRename, CLASSES_DEX_NAME_SANITIZER)) + .isEqualTo(afterRename); + } + + @Test + public void rPackageDexEntryRemover_removesLastDexFile() { + BundleModule module = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) + .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) + .addEntry(createModuleEntryForFile("dex/classes4.dex", new byte[] {0x4})) + .addEntry(createModuleEntryForFile("dex/classes5.dex", new byte[] {0x5})) + .addEntry(createModuleEntryForFile("dex/classes6.dex", new byte[] {0x6})) + .addEntry(createModuleEntryForFile("dex/classes7.dex", new byte[] {0x7})) + .addEntry(createModuleEntryForFile("dex/classes8.dex", new byte[] {0x8})) + .addEntry(createModuleEntryForFile("dex/classes9.dex", new byte[] {0x9})) + .addEntry(createModuleEntryForFile("dex/classes10.dex", new byte[] {0x10})) + .build(); + + BundleModule afterRemoval = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .addEntry(createModuleEntryForFile("dex/classes2.dex", new byte[] {0x2})) + .addEntry(createModuleEntryForFile("dex/classes3.dex", new byte[] {0x3})) + .addEntry(createModuleEntryForFile("dex/classes4.dex", new byte[] {0x4})) + .addEntry(createModuleEntryForFile("dex/classes5.dex", new byte[] {0x5})) + .addEntry(createModuleEntryForFile("dex/classes6.dex", new byte[] {0x6})) + .addEntry(createModuleEntryForFile("dex/classes7.dex", new byte[] {0x7})) + .addEntry(createModuleEntryForFile("dex/classes8.dex", new byte[] {0x8})) + .addEntry(createModuleEntryForFile("dex/classes9.dex", new byte[] {0x9})) + .build(); + + assertThat(new ClassesDexEntriesMutator().applyMutation(module, R_PACKAGE_DEX_ENTRY_REMOVER)) + .isEqualTo(afterRemoval); + } + + @Test + public void rPackageDexEntryRemover_singleDexFile_removes() { + BundleModule module = + createBasicModule() + .addEntry(createModuleEntryForFile("dex/classes.dex", new byte[] {0x1})) + .build(); + + BundleModule afterRemoval = createBasicModule().build(); + + assertThat(new ClassesDexEntriesMutator().applyMutation(module, R_PACKAGE_DEX_ENTRY_REMOVER)) + .isEqualTo(afterRemoval); + } + + @Test + public void rPackageDexEntryRemover_noDexFiles_noChange() { + BundleModule module = createBasicModule().build(); + + assertThat(new ClassesDexEntriesMutator().applyMutation(module, R_PACKAGE_DEX_ENTRY_REMOVER)) + .isEqualTo(module); + } + + private static BundleModule.Builder createBasicModule() { + return BundleModule.builder() + .setName(BundleModuleName.create("module")) + .setBundleType(DEFAULT_BUNDLE_CONFIG.getType()) + .setBundletoolVersion(Version.of(DEFAULT_BUNDLE_CONFIG.getBundletool().getVersion())) + .addEntry(createModuleEntryForFile("assets/hello", new byte[] {0xD, 0xE, 0xA, 0xD})) + .addEntry(createModuleEntryForFile("assets/world", new byte[] {0xB, 0xE, 0xE, 0xF})) + .setAndroidManifestProto(androidManifest("com.test.app", withSplitId("module"))); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java index ac819bde..058761f5 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -97,7 +97,7 @@ @RunWith(JUnit4.class) public class ModuleSplitTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final int VERSION_CODE_RESOURCE_ID = 0x0101021b; @@ -477,12 +477,12 @@ public void forArchive() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) - .addFile("res/drawable/background.jpg", DUMMY_CONTENT) + .addFile("res/drawable/background.jpg", TEST_CONTENT) .build(); AndroidManifest archivedManifest = AndroidManifest.create( androidManifest("com.test.app", ManifestProtoUtils.withVersionCode(123))); - Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(TEST_CONTENT); ModuleSplit split = ModuleSplit.forArchive( @@ -500,7 +500,7 @@ public void forArchive_copiesResourceTable() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) - .addFile("res/drawable/icon.jpg", DUMMY_CONTENT) + .addFile("res/drawable/icon.jpg", TEST_CONTENT) .build(); AndroidManifest archivedManifest = AndroidManifest.create( @@ -510,7 +510,7 @@ public void forArchive_copiesResourceTable() throws Exception { .addPackage("com.test.app") .addDrawableResource("icon", "res/drawable/icon.jpg") .build(); - Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(TEST_CONTENT); ModuleSplit split = ModuleSplit.forArchive( @@ -526,8 +526,8 @@ public void forArchive_filtersResources() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) - .addFile("res/drawable/icon.jpg", DUMMY_CONTENT) - .addFile("res/drawable/background.jpg", DUMMY_CONTENT) + .addFile("res/drawable/icon.jpg", TEST_CONTENT) + .addFile("res/drawable/background.jpg", TEST_CONTENT) .build(); AndroidManifest archivedManifest = AndroidManifest.create( @@ -537,7 +537,7 @@ public void forArchive_filtersResources() throws Exception { .addPackage("com.test.app") .addDrawableResource("icon", "res/drawable/icon.jpg") .build(); - Path archivedClassesDexFile = createTempClassesDexFile(DUMMY_CONTENT); + Path archivedClassesDexFile = createTempClassesDexFile(TEST_CONTENT); ModuleSplit split = ModuleSplit.forArchive( @@ -553,7 +553,7 @@ public void forArchive_usesArchivedClassesFile() throws Exception { BundleModule module = new BundleModuleBuilder("testModule") .setManifest(androidManifest("com.test.app")) - .addFile("dex/classes.dex", DUMMY_CONTENT) + .addFile("dex/classes.dex", TEST_CONTENT) .build(); AndroidManifest archivedManifest = AndroidManifest.create( @@ -643,7 +643,7 @@ public void addUsesSdkLibraryElements_allElementsPresentInManifest() { private ImmutableList fakeEntriesOf(String... entries) { return Arrays.stream(entries) - .map(entry -> createModuleEntryForFile(entry, DUMMY_CONTENT)) + .map(entry -> createModuleEntryForFile(entry, TEST_CONTENT)) .collect(toImmutableList()); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ResourceIdRemapperTest.java b/src/test/java/com/android/tools/build/bundletool/model/ResourceIdRemapperTest.java deleted file mode 100644 index afc264e4..00000000 --- a/src/test/java/com/android/tools/build/bundletool/model/ResourceIdRemapperTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.tools.build.bundletool.model; - -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.android.aapt.Resources.Package; -import com.android.aapt.Resources.PackageId; -import com.android.aapt.Resources.ResourceTable; -import com.android.tools.build.bundletool.testing.BundleModuleBuilder; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for {@link ResourceIdRemapper}. */ -@RunWith(JUnit4.class) -public final class ResourceIdRemapperTest { - - @Test - public void moduleHasNoResourceTable_noChange() { - BundleModule module = - new BundleModuleBuilder("comTestSdk").setManifest(androidManifest("comTestSdk")).build(); - - BundleModule remappedModule = - ResourceIdRemapper.remapResourceIds(module, /* newResourcesPackageId= */ 3); - assertThat(remappedModule).isEqualTo(module); - } - - @Test - public void resourceTableSpecifiesMultiplePackages_throws() { - BundleModule module = - new BundleModuleBuilder("comTestSdk") - .setManifest(androidManifest("comTestSdk")) - .setResourceTable( - ResourceTable.newBuilder() - .addPackage(Package.newBuilder().setPackageId(PackageId.newBuilder().setId(1))) - .addPackage(Package.newBuilder().setPackageId(PackageId.newBuilder().setId(2))) - .build()) - .build(); - - Throwable e = - assertThrows( - IllegalArgumentException.class, - () -> ResourceIdRemapper.remapResourceIds(module, /* newResourcesPackageId= */ 3)); - assertThat(e) - .hasMessageThat() - .contains( - "Module 'comTestSdk' contains resource table with 2 'package' entries, but only 1 entry" - + " is allowed."); - } - - @Test - public void packageIdUpdatedInResourceTable() { - BundleModule module = - new BundleModuleBuilder("comTestSdk") - .setManifest(androidManifest("comTestSdk")) - .setResourceTable( - ResourceTable.newBuilder() - .addPackage(Package.newBuilder().setPackageId(PackageId.newBuilder().setId(1))) - .build()) - .build(); - - module = ResourceIdRemapper.remapResourceIds(module, /* newResourcesPackageId= */ 2); - - assertThat(module.getResourceTable().get().getPackage(0).getPackageId().getId()).isEqualTo(2); - } -} diff --git a/src/test/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapperTest.java b/src/test/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapperTest.java new file mode 100644 index 00000000..8bc7493a --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/ResourceTablePackageIdRemapperTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.USER_PACKAGE_OFFSET; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.entry; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.pkg; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.resourceTable; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.type; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.aapt.Resources.Array; +import com.android.aapt.Resources.Attribute; +import com.android.aapt.Resources.Attribute.Symbol; +import com.android.aapt.Resources.CompoundValue; +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Plural; +import com.android.aapt.Resources.Reference; +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Style; +import com.android.aapt.Resources.Styleable; +import com.android.aapt.Resources.Value; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.android.tools.build.bundletool.testing.ResourcesTableFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ResourceTablePackageIdRemapper}. */ +@RunWith(JUnit4.class) +public final class ResourceTablePackageIdRemapperTest { + + private static final int NEW_PACKAGE_ID = 0x82; + private static final ResourceTablePackageIdRemapper resourceTablePackageIdRemapper = + new ResourceTablePackageIdRemapper(NEW_PACKAGE_ID); + + @Test + public void moduleHasNoResourceTable_noChange() { + BundleModule module = + new BundleModuleBuilder("comTestSdk").setManifest(androidManifest("comTestSdk")).build(); + + BundleModule remappedModule = resourceTablePackageIdRemapper.remap(module); + assertThat(remappedModule).isEqualTo(module); + } + + @Test + public void resourceTableSpecifiesMultiplePackages_throws() { + BundleModule module = + new BundleModuleBuilder("comTestSdk") + .setManifest(androidManifest("comTestSdk")) + .setResourceTable( + ResourcesTableFactory.resourceTable( + pkg(USER_PACKAGE_OFFSET, "pkg1"), pkg(USER_PACKAGE_OFFSET, "pkg2"))) + .build(); + + Throwable e = + assertThrows( + IllegalArgumentException.class, () -> resourceTablePackageIdRemapper.remap(module)); + assertThat(e) + .hasMessageThat() + .contains( + "Module 'comTestSdk' contains resource table with 2 'package' entries, but only 1 entry" + + " is allowed."); + } + + @Test + public void remapInPackageIdAndAllReferences() { + BundleModule module = + new BundleModuleBuilder("comTestSdk") + .setManifest(androidManifest("comTestSdk")) + .setResourceTable(getFullResourceTable(USER_PACKAGE_OFFSET)) + .build(); + + module = resourceTablePackageIdRemapper.remap(module); + + assertThat(module.getResourceTable()).hasValue(getFullResourceTable(NEW_PACKAGE_ID)); + } + + /** Returns resource table with all possible fields of type Reference set. */ + private static ResourceTable getFullResourceTable(int packageId) { + int typeId = 1; + int entryId = 1; + return resourceTable( + pkg( + packageId, + "pkg", + type( + typeId++, + "type" + typeId, + entry( + entryId++, + "entry" + entryId, + ConfigValue.newBuilder() + .setValue(Value.newBuilder().setItem(item(packageId, typeId, entryId))) + .build(), + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue( + CompoundValue.newBuilder() + .setAttr( + Attribute.newBuilder() + .addSymbol( + Symbol.newBuilder() + .setName( + reference( + packageId, typeId, entryId)))))) + .build()), + entry( + entryId++, + "entry" + entryId, + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue( + CompoundValue.newBuilder() + .setStyle( + Style.newBuilder() + .setParent(reference(packageId, typeId, entryId)) + .addEntry( + Style.Entry.newBuilder() + .setKey( + reference( + packageId, typeId, entryId)))))) + .build(), + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue( + CompoundValue.newBuilder() + .setStyleable( + Styleable.newBuilder() + .addEntry( + Styleable.Entry.newBuilder() + .setAttr( + reference( + packageId, typeId, entryId)))))) + .build())), + type( + typeId++, + "type" + typeId, + entry( + entryId++, + "entry" + entryId, + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue( + CompoundValue.newBuilder() + .setArray( + Array.newBuilder() + .addElement( + Array.Element.newBuilder() + .setItem(item(packageId, typeId, entryId))) + .addElement( + Array.Element.newBuilder() + .setItem( + item(packageId, typeId, entryId)))))) + .build(), + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue( + CompoundValue.newBuilder() + .setPlural( + Plural.newBuilder() + .addEntry( + Plural.Entry.newBuilder() + .setItem( + item(packageId, typeId, entryId)))))) + .build())))); + } + + private static Item item(int packageId, int typeId, int entryId) { + return Item.newBuilder().setRef(reference(packageId, typeId, entryId)).build(); + } + + private static Reference reference(int packageId, int typeId, int entryId) { + return Reference.newBuilder().setId(0x1000000 * packageId + 0x10000 * typeId + entryId).build(); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/SdkAsarTest.java b/src/test/java/com/android/tools/build/bundletool/model/SdkAsarTest.java new file mode 100644 index 00000000..d78ec48d --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/SdkAsarTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.DEFAULT_SDK_MODULES_CONFIG; +import static com.android.tools.build.bundletool.testing.TestUtils.DEFAULT_SDK_METADATA; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModules; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkAsarWithModules; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import java.nio.file.Path; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the SdkAsar class. */ +@RunWith(JUnit4.class) +public class SdkAsarTest { + + private static final byte[] TEST_CONTENT = new byte[1]; + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + private Path asarFile; + private Path modulesFile; + + @Before + public void setUp() { + asarFile = tmp.getRoot().toPath().resolve("archive.asar"); + modulesFile = tmp.getRoot().toPath().resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + } + + @Test + public void buildFromZipCreatesExpectedEntries() 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); + + assertThat(sdkAsar.getModule().getEntry(ZipPath.create("dex/classes.dex"))).isPresent(); + assertThat(sdkAsar.getModule().getEntry(ZipPath.create("dex/classes2.dex"))).isPresent(); + assertThat(sdkAsar.getSdkModulesConfig()).isEqualTo(DEFAULT_SDK_MODULES_CONFIG); + assertThat(sdkAsar.getSdkMetadata()).isEqualTo(DEFAULT_SDK_METADATA); + assertThat(sdkAsar.getManifest()).isNotNull(); + } + + @Test + public void buildFromZip_noSdkMetadataPresent_throws() throws Exception { + ZipBuilder modulesBuilder = createZipBuilderForModules(); + modulesBuilder.writeTo(modulesFile); + new ZipBuilder() + .addFileFromDisk(ZipPath.create("modules.resm"), modulesFile.toFile()) + .writeTo(asarFile); + ZipFile sdkAsarZip = new ZipFile(asarFile.toFile()); + ZipFile modulesZip = new ZipFile(modulesFile.toFile()); + + Throwable e = + assertThrows( + InvalidBundleException.class, + () -> SdkAsar.buildFromZip(sdkAsarZip, modulesZip, modulesFile)); + assertThat(e).hasMessageThat().isEqualTo("ASAR is expected to contain 'SdkMetadata.pb' file."); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverterTest.java b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverterTest.java index 9741c50e..ee33be1b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/SdkBundleModuleToAppBundleModuleConverterTest.java @@ -19,12 +19,24 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_SANDBOX_MIN_VERSION; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.USER_PACKAGE_OFFSET; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.entry; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.pkg; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.resourceTable; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.type; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import com.android.aapt.Resources.Package; -import com.android.aapt.Resources.PackageId; +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.FileReference; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Reference; import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Value; +import com.android.aapt.Resources.XmlAttribute; +import com.android.aapt.Resources.XmlElement; +import com.android.aapt.Resources.XmlNode; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; @@ -38,6 +50,7 @@ public final class SdkBundleModuleToAppBundleModuleConverterTest { private static final String PACKAGE_NAME = "com.test.sdk"; + private static final int NEW_PACKAGE_ID = 0x82; @Test public void convert_modifiesModuleName_modifiesManifest_setsIsSdkDependencyModule() { @@ -51,8 +64,9 @@ public void convert_modifiesModuleName_modifiesManifest_setsIsSdkDependencyModul .build(); BundleModule modifiedModule = - SdkBundleModuleToAppBundleModuleConverter.getAppBundleModule( - sdkBundle, RuntimeEnabledSdk.getDefaultInstance()); + new SdkBundleModuleToAppBundleModuleConverter( + sdkBundle, RuntimeEnabledSdk.getDefaultInstance()) + .convert(); // Verify that module name was modified. assertThat(modifiedModule.getName()).isNotEqualTo(sdkBundle.getModule().getName()); @@ -75,30 +89,118 @@ public void convert_modifiesModuleName_modifiesManifest_setsIsSdkDependencyModul @Test public void convert_remapsResourceIdsInResourceTable() { - int originalResourcesPackageId = 1; - int newResourcesPackageId = 2; SdkBundle sdkBundle = new SdkBundleBuilder() .setModule( new BundleModuleBuilder("base") .setManifest( androidManifest(PACKAGE_NAME, withMinSdkVersion(SDK_SANDBOX_MIN_VERSION))) - .setResourceTable( - ResourceTable.newBuilder() - .addPackage( - Package.newBuilder() - .setPackageId( - PackageId.newBuilder().setId(originalResourcesPackageId))) - .build()) + .setResourceTable(resourceTable(pkg(USER_PACKAGE_OFFSET, PACKAGE_NAME))) .build()) .build(); BundleModule modifiedModule = - SdkBundleModuleToAppBundleModuleConverter.getAppBundleModule( - sdkBundle, - RuntimeEnabledSdk.newBuilder().setResourcesPackageId(newResourcesPackageId).build()); + new SdkBundleModuleToAppBundleModuleConverter( + sdkBundle, + RuntimeEnabledSdk.newBuilder().setResourcesPackageId(NEW_PACKAGE_ID).build()) + .convert(); assertThat(modifiedModule.getResourceTable().get().getPackage(0).getPackageId().getId()) - .isEqualTo(newResourcesPackageId); + .isEqualTo(NEW_PACKAGE_ID); + } + + @Test + public void convert_removesLastDexFile() { + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest(PACKAGE_NAME, withMinSdkVersion(SDK_SANDBOX_MIN_VERSION))) + .addFile("dex/classes.dex") + .addFile("dex/classes2.dex") + .build()) + .build(); + + BundleModule modifiedModule = + new SdkBundleModuleToAppBundleModuleConverter( + sdkBundle, RuntimeEnabledSdk.getDefaultInstance()) + .convert(); + + assertThat(modifiedModule.getEntry(ZipPath.create("dex/classes.dex"))).isPresent(); + assertThat(modifiedModule.getEntry(ZipPath.create("dex/classes2.dex"))).isEmpty(); + } + + @Test + public void convert_remapsResourceIdsInXmlResources() throws Exception { + String xmlResourcePath = "res/layout/main.xml"; + ResourceTable resourceTable = resourceTableWithFileReferences(xmlResourcePath); + SdkBundle sdkBundle = + new SdkBundleBuilder() + .setModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest(PACKAGE_NAME, withMinSdkVersion(SDK_SANDBOX_MIN_VERSION))) + .setResourceTable(resourceTable) + .addFile( + xmlResourcePath, + xmlNodeWithResourceReference(USER_PACKAGE_OFFSET).toByteArray()) + .build()) + .build(); + + BundleModule modifiedModule = + new SdkBundleModuleToAppBundleModuleConverter( + sdkBundle, + RuntimeEnabledSdk.newBuilder().setResourcesPackageId(NEW_PACKAGE_ID).build()) + .convert(); + + assertThat( + XmlNode.parseFrom( + modifiedModule + .getEntry(ZipPath.create(xmlResourcePath)) + .get() + .getContent() + .openStream())) + .isEqualTo(xmlNodeWithResourceReference(NEW_PACKAGE_ID)); + } + + private static ResourceTable resourceTableWithFileReferences(String path) { + return resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "pkg", + type( + /* id= */ 1, + "type1", + entry( + /* id= */ 1, + "entry1", + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setItem( + Item.newBuilder() + .setFile( + FileReference.newBuilder() + .setType(FileReference.Type.PROTO_XML) + .setPath(path) + .build()))) + .build())))); + } + + private static XmlNode xmlNodeWithResourceReference(int packageId) { + return XmlNode.newBuilder() + .setElement( + XmlElement.newBuilder() + .addAttribute( + XmlAttribute.newBuilder() + .setCompiledItem(Item.newBuilder().setRef(reference(packageId))))) + .build(); + } + + private static Reference reference(int packageId) { + int typeId = 1; + int entryId = 2; + return Reference.newBuilder().setId(0x1000000 * packageId + 0x10000 * typeId + entryId).build(); } } 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 7daed3a4..5a649ed8 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 @@ -35,7 +35,7 @@ @RunWith(JUnit4.class) public class SdkBundleTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -52,8 +52,8 @@ public void setUp() { public void buildFromZipCreatesExpectedEntries() throws Exception { ZipBuilder modulesBuilder = createZipBuilderForModules() - .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), DUMMY_CONTENT); + .addFileWithContent(ZipPath.create("base/dex/classes1.dex"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes2.dex"), TEST_CONTENT); createZipBuilderForSdkBundleWithModules(modulesBuilder, modulesFile).writeTo(bundleFile); try (ZipFile sdkBundleZip = new ZipFile(bundleFile.toFile()); diff --git a/src/test/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapperTest.java b/src/test/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapperTest.java new file mode 100644 index 00000000..caf61417 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/model/XmlPackageIdRemapperTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlAttribute; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlDecimalIntegerAttribute; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlElement; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlNamespace; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlNode; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.USER_PACKAGE_OFFSET; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.entry; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.pkg; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.resourceTable; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.type; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Entry; +import com.android.aapt.Resources.FileReference; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Reference; +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Value; +import com.android.aapt.Resources.XmlAttribute; +import com.android.aapt.Resources.XmlElement; +import com.android.aapt.Resources.XmlNode; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link XmlPackageIdRemapper}. */ +@RunWith(JUnit4.class) +public final class XmlPackageIdRemapperTest { + + private static final String PACKAGE_NAME = "com.test.sdk"; + private static final String MODULE_NAME = "comTestSdk"; + private static final int NEW_PACKAGE_ID = 0x82; + private static final XmlPackageIdRemapper xmlPackageIgRemapper = + new XmlPackageIdRemapper(NEW_PACKAGE_ID); + + @Test + public void moduleHasNoResourceTable_noChange() { + BundleModule module = + new BundleModuleBuilder(MODULE_NAME).setManifest(androidManifest(MODULE_NAME)).build(); + + BundleModule remappedModule = xmlPackageIgRemapper.remap(module); + + assertThat(remappedModule).isEqualTo(module); + } + + @Test + public void noResourceIdsToRemap_noChange() { + BundleModule module = + new BundleModuleBuilder(MODULE_NAME) + .setManifest(androidManifestWithoutResourceIds()) + .setResourceTable(ResourceTable.getDefaultInstance()) + .build(); + + BundleModule remappedModule = xmlPackageIgRemapper.remap(module); + + assertThat(remappedModule).isEqualTo(module); + } + + @Test + public void remapInXmlResources_resourceTableReferencesXmlFile_fileNotPresentInModule_noChange() { + BundleModule module = + new BundleModuleBuilder(MODULE_NAME) + .setManifest(androidManifestWithoutResourceIds()) + .setResourceTable( + resourceTableWithFileReferences(ImmutableList.of("res/layout/main.xml"))) + .build(); + + BundleModule remappedModule = xmlPackageIgRemapper.remap(module); + + assertThat(remappedModule).isEqualTo(module); + } + + @Test + public void remapInXmlResources_badXmlFileFormat_throws() { + String xmlResourcePath = "res/layout/main.xml"; + BundleModule module = + new BundleModuleBuilder(MODULE_NAME) + .setManifest(androidManifestWithoutResourceIds()) + .setResourceTable(resourceTableWithFileReferences(ImmutableList.of(xmlResourcePath))) + .addFile(xmlResourcePath, new byte[] {1, 2, 3}) + .build(); + + Throwable e = + assertThrows(CommandExecutionException.class, () -> xmlPackageIgRemapper.remap(module)); + assertThat(e).hasMessageThat().contains("Error parsing XML file 'res/layout/main.xml'"); + } + + @Test + public void remapInXmlResources_success() { + ImmutableList xmlResourcePaths = + ImmutableList.of("res/layout/main.xml", "res/layout/info.xml"); + BundleModule module = + new BundleModuleBuilder(MODULE_NAME) + .setManifest(androidManifestWithoutResourceIds()) + .setResourceTable(resourceTableWithFileReferences(xmlResourcePaths)) + .addFile( + "res/layout/main.xml", + xmlNodeWithResourceReferences(USER_PACKAGE_OFFSET).toByteArray()) + .addFile( + "res/layout/info.xml", + xmlNodeWithResourceReferences(USER_PACKAGE_OFFSET).toByteArray()) + // entry that is not referenced in the resource table, and should not be modified. + .addFile("res/raw/raw.xml", new byte[] {1, 2, 3}) + .build(); + + BundleModule modifiedModule = xmlPackageIgRemapper.remap(module); + + assertThat(modifiedModule) + .isEqualTo( + new BundleModuleBuilder(MODULE_NAME) + .setManifest(androidManifestWithoutResourceIds()) + .setResourceTable(resourceTableWithFileReferences(xmlResourcePaths)) + .addFile( + "res/layout/main.xml", + xmlNodeWithResourceReferences(NEW_PACKAGE_ID).toByteArray()) + .addFile( + "res/layout/info.xml", + xmlNodeWithResourceReferences(NEW_PACKAGE_ID).toByteArray()) + .addFile("res/raw/raw.xml", new byte[] {1, 2, 3}) + .build()); + } + + @Test + public void remapInAndroidManifest() { + BundleModule module = + new BundleModuleBuilder(MODULE_NAME) + .setManifest( + androidManifestWithResourceId( + USER_PACKAGE_OFFSET, /* typeId= */ 1, /* entryId= */ 2)) + .setResourceTable(ResourceTable.getDefaultInstance()) + .build(); + + BundleModule remappedModule = xmlPackageIgRemapper.remap(module); + + assertThat(remappedModule) + .isEqualTo( + new BundleModuleBuilder(MODULE_NAME) + .setManifest( + androidManifestWithResourceId( + NEW_PACKAGE_ID, /* typeId= */ 1, /* entryId= */ 2)) + .setResourceTable(ResourceTable.getDefaultInstance()) + .build()); + } + + private static ResourceTable resourceTableWithFileReferences(ImmutableList paths) { + int[] entryId = {0}; + ImmutableList.Builder entries = ImmutableList.builder(); + paths.forEach( + path -> + entries.add( + entry( + ++entryId[0], + "entry" + entryId[0], + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setItem( + Item.newBuilder() + .setFile( + FileReference.newBuilder() + .setType(FileReference.Type.PROTO_XML) + .setPath(path) + .build()))) + .build()))); + return resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "pkg", + type(/* id= */ 1, "type1", entries.build().toArray(new Entry[0])))); + } + + private static XmlNode xmlNodeWithResourceReferences(int packageId) { + int typeId = 1; + int entryId = 1; + return XmlNode.newBuilder() + .setElement( + XmlElement.newBuilder() + .addAttribute( + XmlAttribute.newBuilder() + .setCompiledItem( + Item.newBuilder().setRef(reference(packageId, typeId++, entryId++)))) + .addAttribute( + XmlAttribute.newBuilder() + .setCompiledItem( + Item.newBuilder().setRef(reference(packageId, typeId++, entryId++)))) + .addChild( + XmlNode.newBuilder() + .setElement( + XmlElement.newBuilder() + .addAttribute(XmlAttribute.newBuilder().setName("attributeName")) + .addChild( + XmlNode.newBuilder() + .setElement( + XmlElement.newBuilder() + .addAttribute( + XmlAttribute.newBuilder() + .setCompiledItem( + Item.newBuilder() + .setRef( + reference( + packageId, typeId++, + entryId++)))))) + .addChild( + XmlNode.newBuilder() + .setElement( + XmlElement.newBuilder() + .addAttribute( + XmlAttribute.newBuilder() + .setCompiledItem( + Item.newBuilder() + .setRef( + reference( + packageId, typeId++, + entryId++))))))))) + .build(); + } + + private static Reference reference(int packageId, int typeId, int entryId) { + return Reference.newBuilder().setId(resourceId(packageId, typeId, entryId)).build(); + } + + private static XmlNode androidManifestWithoutResourceIds() { + return xmlNode( + xmlElement( + "manifest", + ImmutableList.of(xmlNamespace("android", ANDROID_NAMESPACE_URI)), + /* attributes= */ ImmutableList.of())); + } + + private static XmlNode androidManifestWithResourceId(int packageId, int typeId, int entryId) { + return xmlNode( + xmlElement( + "manifest", + ImmutableList.of(xmlNamespace("android", ANDROID_NAMESPACE_URI)), + ImmutableList.of( + xmlAttribute("package", PACKAGE_NAME), + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, + "versionCode", + resourceId(packageId, typeId, entryId), + 1)))); + } + + private static int resourceId(int packageId, int typeId, int entryId) { + return 0x1000000 * packageId + 0x10000 * typeId + entryId; + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java index fca77cf3..9290b88f 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ApkSizeUtilsTest.java @@ -49,7 +49,7 @@ @RunWith(JUnit4.class) public class ApkSizeUtilsTest { - private static final byte[] DUMMY_BYTES = new byte[100]; + private static final byte[] TEST_BYTES = new byte[100]; @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Path tmpDir; @@ -161,10 +161,10 @@ public void multipleVariants_withUncompressedEntries() throws Exception { apkTwo)); ZipBuilder archiveBuilder = new ZipBuilder(); - archiveBuilder.addFileWithContent(ZipPath.create(apkOne.toString()), DUMMY_BYTES); + archiveBuilder.addFileWithContent(ZipPath.create(apkOne.toString()), TEST_BYTES); archiveBuilder.addFileWithContent( ZipPath.create(apkTwo.toString()), - DUMMY_BYTES, + TEST_BYTES, EntryOption.UNCOMPRESSED); // APK stored uncompressed in the APKs zip. archiveBuilder.addFileWithProtoContent( ZipPath.create("toc.pb"), BuildApksResult.newBuilder().addAllVariant(variants).build()); diff --git a/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java b/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java index 0cb30193..f5069c0e 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java @@ -51,7 +51,7 @@ @RunWith(JUnit4.class) public class SuffixStripperTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; @Test public void applySuffixStripping_tcf_suffixStrippingEnabled() { @@ -65,11 +65,11 @@ public void applySuffixStripping_tcf_suffixStrippingEnabled() { .setEntries( ImmutableList.of( createModuleEntryForFile( - "assets/textures/untargeted_texture.dat", DUMMY_CONTENT), + "assets/textures/untargeted_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_etc1/etc1_texture.dat", DUMMY_CONTENT), + "assets/textures#tcf_etc1/etc1_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_atc/atc_texture.dat", DUMMY_CONTENT))) + "assets/textures#tcf_atc/atc_texture.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( @@ -117,11 +117,11 @@ public void applySuffixStripping_tcf_suffixStrippingEnabledWithEmptyDefault() { .setEntries( ImmutableList.of( createModuleEntryForFile( - "assets/textures/untargeted_texture.dat", DUMMY_CONTENT), + "assets/textures/untargeted_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_etc1/etc1_texture.dat", DUMMY_CONTENT), + "assets/textures#tcf_etc1/etc1_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_atc/atc_texture.dat", DUMMY_CONTENT))) + "assets/textures#tcf_atc/atc_texture.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( @@ -167,11 +167,11 @@ public void applySuffixStripping_tcf_suffixStrippingDisabled_nonDefaultValuesExc .setEntries( ImmutableList.of( createModuleEntryForFile( - "assets/textures/untargeted_texture.dat", DUMMY_CONTENT), + "assets/textures/untargeted_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_etc1/etc1_texture.dat", DUMMY_CONTENT), + "assets/textures#tcf_etc1/etc1_texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_atc/atc_texture.dat", DUMMY_CONTENT))) + "assets/textures#tcf_atc/atc_texture.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( @@ -217,9 +217,8 @@ public void applySuffixStripping_deviceTier_suffixStrippingEnabled() { .setMasterSplit(true) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/img#tier_0/low_res_image.dat", DUMMY_CONTENT), - createModuleEntryForFile( - "assets/img#tier_1/high_res_image.dat", DUMMY_CONTENT))) + createModuleEntryForFile("assets/img#tier_0/low_res_image.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img#tier_1/high_res_image.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( @@ -263,9 +262,8 @@ public void applySuffixStripping_deviceTier_suffixStrippingDisabled_nonDefaultVa .setMasterSplit(true) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/img#tier_0/low_res_image.dat", DUMMY_CONTENT), - createModuleEntryForFile( - "assets/img#tier_1/high_res_image.dat", DUMMY_CONTENT))) + createModuleEntryForFile("assets/img#tier_0/low_res_image.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img#tier_1/high_res_image.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( @@ -310,10 +308,10 @@ public void removeAssetsTargeting_tcf() { .setMasterSplit(true) .setEntries( ImmutableList.of( - createModuleEntryForFile("assets/untargeted_texture.dat", DUMMY_CONTENT), - createModuleEntryForFile("assets/textures#tcf_etc1/texture.dat", DUMMY_CONTENT), + createModuleEntryForFile("assets/untargeted_texture.dat", TEST_CONTENT), + createModuleEntryForFile("assets/textures#tcf_etc1/texture.dat", TEST_CONTENT), createModuleEntryForFile( - "assets/textures#tcf_etc1/other_texture.dat", DUMMY_CONTENT))) + "assets/textures#tcf_etc1/other_texture.dat", TEST_CONTENT))) .setAssetsConfig( assets( targetedAssetsDirectory( diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGeneratorTest.java index 0d89b872..aa2b2b16 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/SdkRuntimeVariantGeneratorTest.java @@ -15,11 +15,11 @@ */ package com.android.tools.build.bundletool.splitters; -import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkRuntimeVariantTargeting; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_R_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_S_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkRuntimeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMinSdkTargeting; import static com.google.common.truth.Truth.assertThat; @@ -63,7 +63,9 @@ public void bundleHasRuntimeEnabledSdkDeps_generatesSdkRuntimeVariant() { .generate(/* sdkVersionVariantTargetings= */ ImmutableSet.of()); assertThat(sdkRuntimeVariantTargetings) - .containsExactly(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + .containsExactly( + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, /* alternativeSdkVersions= */ ImmutableSet.of())); } @Test @@ -82,7 +84,9 @@ public void noOtherVariantTargetings_generatesOneVariant() { .generate(/* sdkVersionVariantTargetings= */ ImmutableSet.of()); assertThat(sdkRuntimeVariantTargetings) - .containsExactly(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + .containsExactly( + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, /* alternativeSdkVersions= */ ImmutableSet.of())); } @Test @@ -95,7 +99,9 @@ public void otherVariantTargetingsTargetPreT_generatesOneVariant() { variantMinSdkTargeting(ANDROID_S_API_VERSION))); assertThat(sdkRuntimeVariantTargetings) - .containsExactly(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + .containsExactly( + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, /* alternativeSdkVersions= */ ImmutableSet.of())); } @Test @@ -105,7 +111,9 @@ public void otherVariantTargetingTargetsT_generatesOneVariant() { .generate(ImmutableSet.of(variantMinSdkTargeting(ANDROID_T_API_VERSION))); assertThat(sdkRuntimeVariantTargetings) - .containsExactly(sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION)); + .containsExactly( + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, /* alternativeSdkVersions= */ ImmutableSet.of())); } @Test @@ -120,10 +128,24 @@ public void otherVariantTargetingsTargetPostT_generatesOneForEachAndT() { assertThat(sdkRuntimeVariantTargetings) .containsExactly( - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION), - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION + 1), - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION + 2), - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION + 3)); + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, + /* alternativeSdkVersions= */ ImmutableSet.of( + ANDROID_T_API_VERSION + 1, + ANDROID_T_API_VERSION + 2, + ANDROID_T_API_VERSION + 3)), + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION + 1, + /* alternativeSdkVersions= */ ImmutableSet.of( + ANDROID_T_API_VERSION, ANDROID_T_API_VERSION + 2, ANDROID_T_API_VERSION + 3)), + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION + 2, + /* alternativeSdkVersions= */ ImmutableSet.of( + ANDROID_T_API_VERSION, ANDROID_T_API_VERSION + 1, ANDROID_T_API_VERSION + 3)), + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION + 3, + /* alternativeSdkVersions= */ ImmutableSet.of( + ANDROID_T_API_VERSION, ANDROID_T_API_VERSION + 1, ANDROID_T_API_VERSION + 2))); } @Test @@ -137,7 +159,11 @@ public void otherVariantTargetingsTargetPreAndPostT_generatesOneForPostTAndT() { assertThat(sdkRuntimeVariantTargetings) .containsExactly( - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION), - sdkRuntimeVariantTargeting(ANDROID_T_API_VERSION + 1)); + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION, + /* alternativeSdkVersions= */ ImmutableSet.of(ANDROID_T_API_VERSION + 1)), + sdkRuntimeVariantTargeting( + ANDROID_T_API_VERSION + 1, + /* alternativeSdkVersions= */ ImmutableSet.of(ANDROID_T_API_VERSION))); } } 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 495f488a..1372c1ae 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,7 +16,6 @@ package com.android.tools.build.bundletool.splitters; -import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkRuntimeVariantTargeting; 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; @@ -35,6 +34,7 @@ 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.targetedNativeDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMinSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java index 36cf1f9a..66232fd2 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java @@ -53,7 +53,7 @@ /** Helpers related to creating APKs archives in tests. */ public final class ApksArchiveHelpers { - private static final byte[] DUMMY_BYTES = new byte[100]; + private static final byte[] TEST_BYTES = new byte[100]; public static Path createApksArchiveFile(BuildApksResult result, Path location) throws Exception { ZipBuilder archiveBuilder = new ZipBuilder(); @@ -61,7 +61,7 @@ public static Path createApksArchiveFile(BuildApksResult result, Path location) apkDescriptionStream(result) .forEach( apkDesc -> - archiveBuilder.addFileWithContent(ZipPath.create(apkDesc.getPath()), DUMMY_BYTES)); + archiveBuilder.addFileWithContent(ZipPath.create(apkDesc.getPath()), TEST_BYTES)); archiveBuilder.addFileWithProtoContent(ZipPath.create("toc.pb"), result); return archiveBuilder.writeTo(location); @@ -74,7 +74,7 @@ public static Path createApksDirectory(BuildApksResult result, Path location) th for (ApkDescription apkDescription : apkDescriptions) { Path apkPath = location.resolve(apkDescription.getPath()); Files.createDirectories(apkPath.getParent()); - Files.write(apkPath, DUMMY_BYTES); + Files.write(apkPath, TEST_BYTES); } Files.write(location.resolve("toc.pb"), result.toByteArray()); diff --git a/src/test/java/com/android/tools/build/bundletool/testing/AsarUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/AsarUtils.java new file mode 100644 index 00000000..06f8e1b4 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/AsarUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; +import static com.android.tools.build.bundletool.model.SdkAsar.SDK_METADATA_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_MODULES_FILE_NAME; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import com.google.protobuf.ExtensionRegistry; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Utility methods to read data out of ASAR files in useful formats in tests. */ +public final class AsarUtils { + + public static SdkMetadata extractSdkMetadata(ZipFile asarFile) throws Exception { + ZipEntry metadataEntry = asarFile.getEntry(SDK_METADATA_FILE_NAME); + InputStream data = asarFile.getInputStream(metadataEntry); + return SdkMetadata.parseFrom(data, ExtensionRegistry.getEmptyRegistry()); + } + + public static String extractSdkManifest(ZipFile asarFile) throws Exception { + ZipEntry manifestEntry = asarFile.getEntry(MANIFEST_FILENAME); + InputStream data = asarFile.getInputStream(manifestEntry); + return CharStreams.toString(new InputStreamReader(data, UTF_8)); + } + + public static byte[] extractSdkModuleData(ZipFile asarFile) throws Exception { + ZipEntry moduleEntry = asarFile.getEntry(SDK_MODULES_FILE_NAME); + InputStream data = asarFile.getInputStream(moduleEntry); + return ByteStreams.toByteArray(data); + } + + private AsarUtils() {} +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java index c3a304fa..6b38c9fd 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java @@ -29,7 +29,7 @@ import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; -import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryBundleLocation; +import com.android.tools.build.bundletool.model.ModuleEntry.ModuleEntryLocationInZipSource; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNodeBuilder; @@ -87,7 +87,7 @@ public BundleModuleBuilder addFile( ModuleEntry.builder() .setContent(ByteSource.empty()) .setPath(ZipPath.create(relativePath)) - .setBundleLocation(ModuleEntryBundleLocation.create(zipFilePath, entryFullZipPath)) + .setFileLocation(ModuleEntryLocationInZipSource.create(zipFilePath, entryFullZipPath)) .build()); return this; } @@ -106,7 +106,7 @@ public BundleModuleBuilder addFile( ModuleEntry.builder() .setContent(ByteSource.wrap(content)) .setPath(ZipPath.create(relativePath)) - .setBundleLocation(ModuleEntryBundleLocation.create(zipFilePath, entryFullZipPath)) + .setFileLocation(ModuleEntryLocationInZipSource.create(zipFilePath, entryFullZipPath)) .build()); return this; } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java index bde8e9ca..7ff29ecf 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java @@ -77,9 +77,10 @@ public class FakeDevice extends Device { int density, ImmutableList features, ImmutableList glExtensions, - ImmutableMap properties) { + ImmutableMap properties, + Optional androidVersionCodeName) { this.state = state; - this.androidVersion = new AndroidVersion(sdkVersion); + this.androidVersion = new AndroidVersion(sdkVersion, androidVersionCodeName.orElse(null)); this.abis = abis; this.density = density; this.serialNumber = serialNumber; @@ -102,7 +103,10 @@ public static FakeDevice fromDeviceSpecWithProperties( deviceSpec.getScreenDensity(), ImmutableList.copyOf(deviceSpec.getDeviceFeaturesList()), ImmutableList.copyOf(deviceSpec.getGlExtensionsList()), - properties); + properties, + deviceSpec.getCodename().isEmpty() + ? Optional.empty() + : Optional.of(deviceSpec.getCodename())); device.injectShellCommandOutput( "pm list features", () -> @@ -176,7 +180,8 @@ public static FakeDevice inDisconnectedState(String deviceId, DeviceState device /* density= */ -1, ImmutableList.of(), ImmutableList.of(), - ImmutableMap.of()); + ImmutableMap.of(), + Optional.empty()); } @Override diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java index 57215d19..42dd6344 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java @@ -932,12 +932,24 @@ public static VariantTargeting lPlusVariantTargeting() { } public static VariantTargeting sdkRuntimeVariantTargeting() { + return sdkRuntimeVariantTargeting(Versions.ANDROID_T_API_VERSION); + } + + public static VariantTargeting sdkRuntimeVariantTargeting(int androidSdkVersion) { + return sdkRuntimeVariantTargeting( + androidSdkVersion, /* alternativeSdkVersions= */ ImmutableSet.of()); + } + + public static VariantTargeting sdkRuntimeVariantTargeting( + int androidSdkVersion, ImmutableSet alternativeSdkVersions) { return VariantTargeting.newBuilder() .setSdkRuntimeTargeting(SdkRuntimeTargeting.newBuilder().setRequiresSdkRuntime(true)) .setSdkVersionTargeting( sdkVersionTargeting( - sdkVersionFrom(Versions.ANDROID_T_API_VERSION), - /* alternatives= */ ImmutableSet.of())) + sdkVersionFrom(androidSdkVersion), + alternativeSdkVersions.stream() + .map(TargetingUtils::sdkVersionFrom) + .collect(toImmutableSet()))) .build(); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java index 7f42936e..1b42fcb4 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 @@ -127,6 +127,7 @@ public static class Builder { @Nullable private ApkBuildMode apkBuildMode; @Nullable private String[] moduleNames; @Nullable private DeviceSpec deviceSpec; + @Nullable private Boolean fuseOnlyDeviceMatchingModules; @Nullable private Consumer buildApksCommandSetter; @Nullable private OptimizationDimension[] optimizationDimensions; @Nullable private PrintStream printStream; @@ -214,6 +215,11 @@ public Builder withApkBuildMode(ApkBuildMode apkBuildMode) { return this; } + public Builder withFuseOnlyDeviceMatchingModules(boolean enabled) { + this.fuseOnlyDeviceMatchingModules = enabled; + return this; + } + public Builder withModules(String... moduleNames) { this.moduleNames = moduleNames; return this; @@ -351,6 +357,9 @@ public TestModule build() { if (deviceSpec != null) { command.setDeviceSpec(deviceSpec); } + if (fuseOnlyDeviceMatchingModules != null) { + command.setFuseOnlyDeviceMatchingModules(fuseOnlyDeviceMatchingModules); + } if (optimizationDimensions != null) { command.setOptimizationDimensions(ImmutableSet.copyOf(optimizationDimensions)); } 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 de222bf9..a5896e1d 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 @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.testing; +import static com.android.tools.build.bundletool.model.SdkAsar.SDK_METADATA_FILE_NAME; import static com.android.tools.build.bundletool.model.utils.BundleParser.SDK_BUNDLE_CONFIG_FILE_NAME; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallLocation; @@ -27,6 +28,7 @@ import com.android.aapt.Resources.XmlNode; import com.android.bundle.SdkBundleConfigProto.SdkBundleConfig; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.model.AndroidManifest; @@ -60,9 +62,10 @@ /** Some misc utility methods for tests. */ public final class TestUtils { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; private static final String PACKAGE_NAME = "com.test.sdk.detail"; private static final XmlNode MANIFEST = createSdkAndroidManifest(); + public static final SdkMetadata DEFAULT_SDK_METADATA = getSdkMetadata(); /** Tests that missing mandatory property is detected by an AutoValue.Builder. */ public static void expectMissingRequiredBuilderPropertyException( @@ -193,6 +196,18 @@ public static ZipBuilder createZipBuilderForSdkBundleWithModules( .addFileFromDisk(ZipPath.create("modules.resm"), modulesPath.toFile()); } + public static ZipBuilder createZipBuilderForSdkAsarWithModules( + ZipBuilder modules, Path modulesPath) throws IOException { + return createZipBuilderForSdkAsarWithModules(modules, DEFAULT_SDK_METADATA, modulesPath); + } + + public static ZipBuilder createZipBuilderForSdkAsarWithModules( + ZipBuilder modules, SdkMetadata sdkMetadata, Path modulesPath) throws IOException { + modules.writeTo(modulesPath); + return createZipBuilderForSdkAsar(sdkMetadata) + .addFileFromDisk(ZipPath.create("modules.resm"), modulesPath.toFile()); + } + public static ZipBuilder createZipBuilderForSdkBundle() { return new ZipBuilder() .addFileWithContent( @@ -201,17 +216,26 @@ public static ZipBuilder createZipBuilderForSdkBundle() { ZipPath.create(SDK_BUNDLE_CONFIG_FILE_NAME), SdkBundleConfig.getDefaultInstance()); } + public static ZipBuilder createZipBuilderForSdkAsar() { + return createZipBuilderForSdkAsar(DEFAULT_SDK_METADATA); + } + + public static ZipBuilder createZipBuilderForSdkAsar(SdkMetadata sdkMetadata) { + return new ZipBuilder() + .addFileWithProtoContent(ZipPath.create(SDK_METADATA_FILE_NAME), sdkMetadata); + } + public static ZipBuilder createZipBuilderForModules() { return new ZipBuilder() .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) .addFileWithContent( ZipPath.create("SdkModulesConfig.pb"), DEFAULT_SDK_MODULES_CONFIG.toByteArray()); } public static ZipBuilder createZipBuilderForModulesWithoutManifest() { return new ZipBuilder() - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) .addFileWithContent( ZipPath.create("SdkModulesConfig.pb"), DEFAULT_SDK_MODULES_CONFIG.toByteArray()); } @@ -220,7 +244,7 @@ public static ZipBuilder createZipBuilderForModulesWithInvalidManifest() { return new ZipBuilder() .addFileWithProtoContent( ZipPath.create("base/manifest/AndroidManifest.xml"), createInvalidSdkAndroidManifest()) - .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) .addFileWithContent( ZipPath.create("SdkModulesConfig.pb"), DEFAULT_SDK_MODULES_CONFIG.toByteArray()); } @@ -233,4 +257,11 @@ public static XmlNode createInvalidSdkAndroidManifest() { return androidManifest( PACKAGE_NAME, withMinSdkVersion(32), withInstallLocation("preferExternal")); } + + private static SdkMetadata getSdkMetadata() { + return SdkMetadata.newBuilder() + .setPackageName(PACKAGE_NAME) + .setSdkVersion(DEFAULT_SDK_MODULES_CONFIG.getSdkVersion()) + .build(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/BundleZipValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/BundleZipValidatorTest.java index a2be7ed4..ab1a0883 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/BundleZipValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/BundleZipValidatorTest.java @@ -22,9 +22,13 @@ import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.zipflinger.Sources; +import com.android.zipflinger.ZipArchive; +import java.io.ByteArrayInputStream; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.junit.Before; @@ -37,7 +41,7 @@ @RunWith(JUnit4.class) public class BundleZipValidatorTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; @Rule public TemporaryFolder tmp = new TemporaryFolder(); private Path tempFolder; @@ -68,11 +72,38 @@ public void validateBundleZipEntry_directory_throws() throws Exception { } } + @Test + public void validateBundleZipEntry_fileWithLeadingSlash_throws() throws Exception { + + Path bundlePath = tempFolder.resolve("bundle.aab"); + + try (ZipArchive zipArchive = new ZipArchive(bundlePath)) { + zipArchive.add( + Sources.from( + new ByteArrayInputStream(TEST_CONTENT), "/file.txt", Deflater.DEFAULT_COMPRESSION)); + } + + try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { + ArrayList entries = Collections.list(bundleZip.entries()); + // Sanity check. + assertThat(entries).hasSize(1); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new BundleZipValidator().validateBundleZipEntry(bundleZip, entries.get(0))); + + assertThat(exception) + .hasMessageThat() + .contains("zip file contains a zip entry starting with /"); + } + } + @Test public void validateBundleZipEntry_file_ok() throws Exception { Path bundlePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java index fe8750b6..5403184f 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/MandatoryFilesPresenceValidatorTest.java @@ -35,7 +35,7 @@ @RunWith(JUnit4.class) public class MandatoryFilesPresenceValidatorTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; @Rule public TemporaryFolder tmp = new TemporaryFolder(); private Path tempFolder; @@ -49,7 +49,7 @@ public void setUp() { public void moduleZipFile_withoutAndroidManifest_throws() throws Exception { Path modulePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("assets/file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("base.zip")); try (ZipFile moduleZip = new ZipFile(modulePath.toFile())) { @@ -70,7 +70,7 @@ public void moduleZipFile_withoutAndroidManifest_throws() throws Exception { public void moduleZipFile_withAllMandatoryFiles_ok() throws Exception { Path modulePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("manifest/AndroidManifest.xml"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("manifest/AndroidManifest.xml"), TEST_CONTENT) .writeTo(tempFolder.resolve("base.zip")); try (ZipFile moduleZip = new ZipFile(modulePath.toFile())) { @@ -83,7 +83,7 @@ public void moduleZipFile_withAllMandatoryFiles_ok() throws Exception { public void bundleZipFile_withoutBundleConfig_throws() throws Exception { Path bundlePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("base/manifest/AndroidManifest.xml"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/manifest/AndroidManifest.xml"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -102,8 +102,8 @@ public void bundleZipFile_withoutBundleConfig_throws() throws Exception { public void bundleZipFile_withoutAndroidManifestInModule_throws() throws Exception { Path bundlePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("base/assets/file.txt"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("BundleConfig.pb"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/assets/file.txt"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("BundleConfig.pb"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -124,8 +124,8 @@ public void bundleZipFile_withoutAndroidManifestInModule_throws() throws Excepti public void bundleZipFile_withAllMandatoryFiles_ok() throws Exception { Path bundlePath = new ZipBuilder() - .addFileWithContent(ZipPath.create("base/manifest/AndroidManifest.xml"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("BundleConfig.pb"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("base/manifest/AndroidManifest.xml"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("BundleConfig.pb"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java index 9cda1834..4425a696 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkBundleHasOneModuleValidatorTest.java @@ -35,7 +35,7 @@ @RunWith(JUnit4.class) public class SdkBundleHasOneModuleValidatorTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; @Rule public TemporaryFolder tmp = new TemporaryFolder(); private Path tempFolder; @@ -50,7 +50,7 @@ public void sdkModulesZipFile_multipleModules_throws() throws Exception { Path modulesPath = createZipBuilderForModules() .addFileWithContent( - ZipPath.create("feature/manifest/AndroidManifest.xml"), DUMMY_CONTENT) + ZipPath.create("feature/manifest/AndroidManifest.xml"), TEST_CONTENT) .writeTo(tempFolder.resolve(EXTRACTED_SDK_MODULES_FILE_NAME)); try (ZipFile modulesZip = new ZipFile(modulesPath.toFile())) { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java index 0197e42e..1f0b1564 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java @@ -55,7 +55,7 @@ @RunWith(JUnit4.class) public class ValidatorRunnerTest { - private static final byte[] DUMMY_CONTENT = new byte[1]; + private static final byte[] TEST_CONTENT = new byte[1]; public static final BundleConfig BUNDLE_CONFIG = BundleConfigBuilder.create().build(); @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -75,7 +75,7 @@ public void validateBundleZipFile_invokesRightSubValidatorMethods() throws Excep Path bundlePath = new ZipBuilder() .addDirectory(ZipPath.create("directory")) - .addFileWithContent(ZipPath.create("file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -98,7 +98,7 @@ public void validateSdkBundleZipFile_invokesRightSubValidatorMethods() throws Ex Path bundlePath = new ZipBuilder() .addDirectory(ZipPath.create("directory")) - .addFileWithContent(ZipPath.create("file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.asb")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -130,12 +130,12 @@ public void validateBundle_invokesRightSubValidatorMethods() throws Exception { ZipPath.create("moduleX/native.pb"), NativeLibraries.getDefaultInstance()) .addFileWithProtoContent( ZipPath.create("moduleX/resources.pb"), ResourceTable.getDefaultInstance()) - .addFileWithContent(ZipPath.create("moduleX/res/drawable/icon.png"), DUMMY_CONTENT) - .addFileWithContent(ZipPath.create("moduleX/lib/x86/libX.so"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("moduleX/res/drawable/icon.png"), TEST_CONTENT) + .addFileWithContent(ZipPath.create("moduleX/lib/x86/libX.so"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("moduleY/manifest/AndroidManifest.xml"), androidManifest("com.test.app", withSplitId("moduleY"))) - .addFileWithContent(ZipPath.create("moduleY/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("moduleY/assets/file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -171,11 +171,11 @@ public void validateBundle_invokesSubValidatorsInSequence() throws Exception { .addFileWithProtoContent( ZipPath.create("moduleX/manifest/AndroidManifest.xml"), androidManifest("com.test.app", withSplitId("moduleX"))) - .addFileWithContent(ZipPath.create("moduleX/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("moduleX/assets/file.txt"), TEST_CONTENT) .addFileWithProtoContent( ZipPath.create("moduleY/manifest/AndroidManifest.xml"), androidManifest("com.test.app", withSplitId("moduleY"))) - .addFileWithContent(ZipPath.create("moduleY/assets/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("moduleY/assets/file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("bundle.aab")); try (ZipFile bundleZip = new ZipFile(bundlePath.toFile())) { @@ -206,7 +206,7 @@ public void validateModuleZipFile_invokesRightSubValidatorMethods() throws Excep Path modulePath = new ZipBuilder() .addDirectory(ZipPath.create("module")) - .addFileWithContent(ZipPath.create("module/file.txt"), DUMMY_CONTENT) + .addFileWithContent(ZipPath.create("module/file.txt"), TEST_CONTENT) .writeTo(tempFolder.resolve("module.zip")); try (ZipFile moduleZip = new ZipFile(modulePath.toFile())) {