diff --git a/README.md b/README.md index 345afd8d..204c2093 100755 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ Read more about the App Bundle format and Bundletool's usage at ## Releases -Latest release: [0.7.1](https://github.com/google/bundletool/releases) +Latest release: [0.7.2](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 6fe2b6d0..3e19c6c8 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 0.7.1 +release_version = 0.7.2 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 7eeb9d5a..dace8579 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -16,6 +16,10 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.DEFAULT; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM_COMPRESSED; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.google.common.base.Preconditions.checkArgument; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; @@ -38,6 +42,7 @@ import com.android.tools.build.bundletool.utils.flags.Flag.Password; import com.android.tools.build.bundletool.utils.flags.ParsedFlags; import com.google.auto.value.AutoValue; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableSet; import com.google.common.io.MoreFiles; import com.google.common.util.concurrent.ListeningExecutorService; @@ -58,14 +63,30 @@ public abstract class BuildApksCommand { public static final String COMMAND_NAME = "build-apks"; + /** Modes to run {@link BuildApksCommand} against to generate APKs. */ + public enum ApkBuildMode { + /** DEFAULT mode generates split, standalone and instant APKs. */ + DEFAULT, + /** UNIVERSAL mode generates universal APK. */ + UNIVERSAL, + /** SYSTEM mode generates APKs for the system image. */ + SYSTEM, + /** + * SYSTEM_COMPRESSED mode generates compressed APK and an additional uncompressed stub APK + * (containing only android manifest) for the system image. + */ + SYSTEM_COMPRESSED + } + private static final Flag BUNDLE_LOCATION_FLAG = Flag.path("bundle"); private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); private static final Flag OVERWRITE_OUTPUT_FLAG = Flag.booleanFlag("overwrite"); private static final Flag> OPTIMIZE_FOR_FLAG = Flag.enumSet("optimize-for", OptimizationDimension.class); private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); - private static final Flag GENERATE_UNIVERSAL_APK_FLAG = Flag.booleanFlag("universal"); private static final Flag MAX_THREADS_FLAG = Flag.positiveInteger("max-threads"); + private static final Flag MODE_FLAG = Flag.enumFlag("mode", ApkBuildMode.class); + private static final Flag GENERATE_UNIVERSAL_APK_FLAG = Flag.booleanFlag("universal"); private static final Flag ADB_PATH_FLAG = Flag.path("adb"); private static final Flag CONNECTED_DEVICE_FLAG = Flag.booleanFlag("connected-device"); @@ -106,7 +127,7 @@ public abstract class BuildApksCommand { /** Required when getGenerateOnlyForConnectedDevice is true. */ public abstract Optional getAdbPath(); - public abstract boolean getGenerateOnlyUniversalApk(); + public abstract ApkBuildMode getApkBuildMode(); public abstract Optional getAapt2Command(); @@ -129,10 +150,12 @@ ListeningExecutorService getExecutorService() { public abstract Optional getFirstVariantNumber(); + public abstract Optional getOutputPrintStream(); + public static Builder builder() { return new AutoValue_BuildApksCommand.Builder() .setOverwriteOutput(false) - .setGenerateOnlyUniversalApk(false) + .setApkBuildMode(DEFAULT) .setGenerateOnlyForConnectedDevice(false) .setCreateApkSetArchive(true) .setOptimizationDimensions(ImmutableSet.of()); @@ -161,11 +184,11 @@ public abstract Builder setOptimizationDimensions( ImmutableSet optimizationDimensions); /** - * Sets whether a universal APK should be generated. + * Sets against which mode APK should be generated. * - *

The default is false. If this is set to {@code true}, no other APKs will be generated. + *

By default we generate split, standalone ans instant APKs. */ - public abstract Builder setGenerateOnlyUniversalApk(boolean universalOnly); + public abstract Builder setApkBuildMode(ApkBuildMode mode); /** * Sets if the generated APK Set will contain APKs compatible only with the connected device. @@ -254,6 +277,9 @@ public Builder setExecutorService(ListeningExecutorService executorService) { */ public abstract Builder setFirstVariantNumber(int firstVariantNumber); + /** For command line, sets the {@link PrintStream} to use for outputting the warnings. */ + public abstract Builder setOutputPrintStream(PrintStream outputPrintStream); + abstract BuildApksCommand autoBuild(); public BuildApksCommand build() { @@ -263,20 +289,27 @@ public BuildApksCommand build() { } BuildApksCommand command = autoBuild(); - if (!command.getOptimizationDimensions().isEmpty() && command.getGenerateOnlyUniversalApk()) { + if (!command.getOptimizationDimensions().isEmpty() + && !command.getApkBuildMode().equals(DEFAULT)) { throw new ValidationException( - "Cannot generate universal APK and specify optimization dimensions at the same time."); + String.format( + "Optimization dimension can be only set when running with '%s' mode flag.", + Ascii.toLowerCase(DEFAULT.name()))); } - if (command.getGenerateOnlyForConnectedDevice() && command.getGenerateOnlyUniversalApk()) { + if (command.getGenerateOnlyForConnectedDevice() + && !command.getApkBuildMode().equals(DEFAULT)) { throw new ValidationException( - "Cannot generate universal APK and optimize for the connected device " - + "at the same time."); + String.format( + "Optimizing for connected device only possible when running with '%s' mode flag.", + Ascii.toLowerCase(DEFAULT.name()))); } - if (command.getDeviceSpecPath().isPresent() && command.getGenerateOnlyUniversalApk()) { + if (command.getDeviceSpecPath().isPresent() && !command.getApkBuildMode().equals(DEFAULT)) { throw new ValidationException( - "Cannot generate universal APK and optimize for the device spec at the same time."); + String.format( + "Optimizing for device spec only possible when running with '%s' mode flag.", + Ascii.toLowerCase(DEFAULT.name()))); } if (command.getGenerateOnlyForConnectedDevice() && command.getDeviceSpecPath().isPresent()) { @@ -316,7 +349,8 @@ static BuildApksCommand fromFlags( BuildApksCommand.Builder buildApksCommand = BuildApksCommand.builder() .setBundlePath(BUNDLE_LOCATION_FLAG.getRequiredValue(flags)) - .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); + .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)) + .setOutputPrintStream(out); // Optional arguments. OVERWRITE_OUTPUT_FLAG.getValue(flags).ifPresent(buildApksCommand::setOverwriteOutput); @@ -325,9 +359,16 @@ static BuildApksCommand fromFlags( .ifPresent( aapt2Path -> buildApksCommand.setAapt2Command(Aapt2Command.createFromExecutablePath(aapt2Path))); - GENERATE_UNIVERSAL_APK_FLAG - .getValue(flags) - .ifPresent(buildApksCommand::setGenerateOnlyUniversalApk); + + if (GENERATE_UNIVERSAL_APK_FLAG.getValue(flags).orElse(false)) { + out.printf( + "WARNING: The '%s' flag is now replaced with --mode=universal and is going to be removed " + + "in the next BundleTool version.", + GENERATE_UNIVERSAL_APK_FLAG.getName()); + buildApksCommand.setApkBuildMode(UNIVERSAL); + } + + MODE_FLAG.getValue(flags).ifPresent(buildApksCommand::setApkBuildMode); MAX_THREADS_FLAG .getValue(flags) .ifPresent( @@ -370,21 +411,20 @@ static BuildApksCommand fromFlags( // Applied only when --connected-device flag is set, because we don't want to fail command // if ADB cannot be found in a normal mode. + Optional adbPathFromFlag = ADB_PATH_FLAG.getValue(flags); if (connectedDeviceMode) { Path adbPath = - ADB_PATH_FLAG - .getValue(flags) - .orElseGet( - () -> - environmentVariableProvider - .getVariable(ANDROID_HOME_VARIABLE) - .flatMap(path -> new SdkToolsLocator().locateAdb(Paths.get(path))) - .orElseThrow( - () -> - new CommandExecutionException( - "Unable to determine the location of ADB. Please set the " - + "--adb flag or define ANDROID_HOME environment " - + "variable."))); + adbPathFromFlag.orElseGet( + () -> + environmentVariableProvider + .getVariable(ANDROID_HOME_VARIABLE) + .flatMap(path -> new SdkToolsLocator().locateAdb(Paths.get(path))) + .orElseThrow( + () -> + new CommandExecutionException( + "Unable to determine the location of ADB. Please set the " + + "--adb flag or define ANDROID_HOME environment " + + "variable."))); buildApksCommand.setAdbPath(adbPath).setAdbServer(adbServer); } @@ -447,12 +487,22 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(GENERATE_UNIVERSAL_APK_FLAG.getName()) + .setFlagName(MODE_FLAG.getName()) + .setExampleValue(joinFlagOptions(ApkBuildMode.values())) .setOptional(true) .setDescription( - "If set, will generate only a single universal APK. This flag is mutually " - + "exclusive with flag --%s.", - OPTIMIZE_FOR_FLAG.getName()) + "Specifies which mode to run '%s' command against. Acceptable values are '%s'. " + + "If not set or set to '%s' we generate split, standalone and instant " + + "APKs. If set to '%s' we generate universal APK. If set to '%s' we " + + "generate APKs for system image. If set to '%s' we generate compressed " + + "APK and an additional uncompressed stub APK (containing only Android " + + "manifest) for the system image.", + BuildApksCommand.COMMAND_NAME, + joinFlagOptions(ApkBuildMode.values()), + Ascii.toLowerCase(DEFAULT.name()), + Ascii.toLowerCase(UNIVERSAL.name()), + Ascii.toLowerCase(SYSTEM.name()), + Ascii.toLowerCase(SYSTEM_COMPRESSED.name())) .build()) .addFlag( FlagDescription.builder() @@ -470,10 +520,11 @@ public static CommandHelp help() { .setOptional(true) .setDescription( "If set, will generate APKs with optimizations for the given dimensions. " - + "Acceptable values are '%s'. This flag is mutually exclusive with flag " - + "--%s.", + + "Acceptable values are '%s'. This flag should be only be set with " + + "--%s=%s flag.", joinFlagOptions(OptimizationDimension.values()), - GENERATE_UNIVERSAL_APK_FLAG.getName()) + MODE_FLAG.getName(), + Ascii.toLowerCase(DEFAULT.name())) .build()) .addFlag( FlagDescription.builder() @@ -526,7 +577,8 @@ public static CommandHelp help() { .setDescription( "If set, will generate APK Set optimized for the connected device. The " + "generated APK Set will only be installable on that specific class of " - + "devices.") + + "devices. This flag should be only be set with --%s=%s flag.", + MODE_FLAG.getName(), Ascii.toLowerCase(DEFAULT.name())) .build()) .addFlag( FlagDescription.builder() @@ -556,8 +608,11 @@ public static CommandHelp help() { .setOptional(true) .setDescription( "Path to the device spec file generated by the '%s' command. If present, " - + "it will generate an APK Set optimized for the specified device spec.", - GetDeviceSpecCommand.COMMAND_NAME) + + "it will generate an APK Set optimized for the specified device spec. " + + "This flag should be only be set with --%s=%s flag.", + GetDeviceSpecCommand.COMMAND_NAME, + MODE_FLAG.getName(), + Ascii.toLowerCase(DEFAULT.name())) .build()) .build(); } 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 8b0462ff..5719922e 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -15,7 +15,6 @@ */ package com.android.tools.build.bundletool.commands; -import static com.android.tools.build.bundletool.utils.TargetingProtoUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.utils.files.FilePreconditions.checkDirectoryExists; import static com.android.tools.build.bundletool.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.android.tools.build.bundletool.utils.files.FilePreconditions.checkFileExistsAndExecutable; @@ -29,9 +28,6 @@ import com.android.bundle.Config.Bundletool; import com.android.bundle.Config.Compression; import com.android.bundle.Devices.DeviceSpec; -import com.android.bundle.Targeting.ApkTargeting; -import com.android.bundle.Targeting.SdkVersionTargeting; -import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.device.AdbServer; import com.android.tools.build.bundletool.device.DeviceAnalyzer; import com.android.tools.build.bundletool.device.DeviceSpecParser; @@ -46,16 +42,14 @@ import com.android.tools.build.bundletool.model.ApkListener; import com.android.tools.build.bundletool.model.ApkModifier; import com.android.tools.build.bundletool.model.AppBundle; -import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.GeneratedApks; -import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.optimizations.OptimizationsMerger; import com.android.tools.build.bundletool.splitters.ApkGenerationConfiguration; -import com.android.tools.build.bundletool.splitters.BundleSharder; +import com.android.tools.build.bundletool.splitters.ShardedApksGenerator; import com.android.tools.build.bundletool.splitters.SplitApksGenerator; import com.android.tools.build.bundletool.targeting.AlternativeVariantTargetingPopulator; import com.android.tools.build.bundletool.utils.SdkToolsLocator; @@ -102,90 +96,113 @@ public Path execute(Path tempDir) { AppBundle appBundle = AppBundle.buildFromZip(bundleZip); bundleValidator.validate(appBundle); + if (appBundle.has32BitRenderscriptCode()) { + printWarning( + "App Bundle contains 32-bit RenderScript bitcode file (.bc) which disables 64-bit " + + "support in Android. 64-bit native libraries won't be included in generated " + + "APKs."); + } + BundleConfig bundleConfig = appBundle.getBundleConfig(); Version bundleVersion = BundleToolVersion.getVersionFromBundleConfig(bundleConfig); ImmutableList allModules = ImmutableList.copyOf(appBundle.getModules().values()); - ApkSetBuilder apkSetBuilder = - createApkSetBuilder( - aapt2Command, - command.getSigningConfiguration(), - bundleConfig.getCompression(), - tempDir); - - ApkOptimizations apkOptimizations = - command.getGenerateOnlyUniversalApk() - ? ApkOptimizations.getOptimizationsForUniversalApk() - : new OptimizationsMerger() - .mergeWithDefaults(bundleConfig, command.getOptimizationDimensions()); - - boolean generateSplitApks = - !command.getGenerateOnlyUniversalApk() && !targetsOnlyPreL(appBundle); - boolean generateStandaloneApks = - command.getGenerateOnlyUniversalApk() || targetsPreL(appBundle); + GeneratedApks.Builder generatedApksBuilder = GeneratedApks.builder(); + switch (command.getApkBuildMode()) { + case DEFAULT: + boolean isApexBundle = appBundle.getBaseModule().getApexConfig().isPresent(); + boolean generateSplitApks = !targetsOnlyPreL(appBundle) && !isApexBundle; + boolean generateStandaloneApks = targetsPreL(appBundle) || isApexBundle; + + if (deviceSpec.isPresent()) { + if (deviceSpec.get().getSdkVersion() >= Versions.ANDROID_L_API_VERSION) { + generateStandaloneApks = false; + if (!generateSplitApks) { + throw new CommandExecutionException( + "App Bundle targets pre-L devices, but the device has SDK version higher " + + "or equal to L."); + } + } else { + generateSplitApks = false; + if (!generateStandaloneApks) { + throw new CommandExecutionException( + "App Bundle targets L+ devices, but the device has SDK version lower than L."); + } + } + } - if (deviceSpec.isPresent()) { - if (deviceSpec.get().getSdkVersion() >= Versions.ANDROID_L_API_VERSION) { - generateStandaloneApks = false; - if (!generateSplitApks) { - throw new CommandExecutionException( - "App Bundle targets pre-L devices, but the device has SDK version higher " - + "or equal to L."); + if (generateSplitApks) { + ApkGenerationConfiguration.Builder apkGenerationConfiguration = + getApkGenerationConfigurationBuilder(appBundle, bundleConfig, bundleVersion); + + generatedApksBuilder.setSplitApks( + new SplitApksGenerator( + allModules, + bundleVersion, + apkGenerationConfiguration.setForInstantAppVariants(false).build()) + .generateSplits()); + + // Generate instant splits for any instant compatible modules. + ImmutableList instantModules = + allModules.stream() + .filter(BundleModule::isInstantModule) + .collect(toImmutableList()); + + generatedApksBuilder.setInstantApks( + new SplitApksGenerator( + instantModules, + bundleVersion, + apkGenerationConfiguration + .setForInstantAppVariants(true) + // We can't enable this splitter for instant APKs, as currently they + // only support one variant. + .setEnableDexCompressionSplitter(false) + .build()) + .generateSplits()); } - } else { - generateSplitApks = false; - if (!generateStandaloneApks) { - throw new CommandExecutionException( - "App Bundle targets L+ devices, but the device has SDK version lower than L."); + if (generateStandaloneApks) { + ShardedApksGenerator shardedApksGenerator = + new ShardedApksGenerator( + tempDir, + bundleVersion, + SplitType.STANDALONE, + /* generate64BitShards= */ !appBundle.has32BitRenderscriptCode()); + generatedApksBuilder.setStandaloneApks( + isApexBundle + ? shardedApksGenerator.generateApexSplits(modulesToFuse(allModules)) + : shardedApksGenerator.generateSplits( + modulesToFuse(allModules), + appBundle.getBundleMetadata(), + getApkOptimizations(bundleConfig))); } - } + break; + case UNIVERSAL: + // Note: Universal APK is a special type of standalone, with no optimization dimensions. + generatedApksBuilder.setStandaloneApks( + new ShardedApksGenerator(tempDir, bundleVersion) + .generateSplits( + modulesToFuse(allModules), + appBundle.getBundleMetadata(), + ApkOptimizations.getOptimizationsForUniversalApk())); + break; + case SYSTEM_COMPRESSED: + case SYSTEM: + // Generate system APKs. + generatedApksBuilder.setSystemApks( + new ShardedApksGenerator( + tempDir, + bundleVersion, + SplitType.SYSTEM, + /* generate64BitShards= */ !appBundle.has32BitRenderscriptCode()) + .generateSplits( + modulesToFuse(allModules), + appBundle.getBundleMetadata(), + getApkOptimizations(bundleConfig))); + break; } - GeneratedApks.Builder generatedApksBuilder = GeneratedApks.builder(); - if (generateSplitApks) { - ApkGenerationConfiguration.Builder apkGenerationConfiguration = - ApkGenerationConfiguration.builder() - .setOptimizationDimensions(apkOptimizations.getSplitDimensions()); - boolean enableNativeLibraryCompressionSplitter = - apkOptimizations.getUncompressNativeLibraries(); - apkGenerationConfiguration.setEnableNativeLibraryCompressionSplitter( - enableNativeLibraryCompressionSplitter); - generatedApksBuilder.setSplitApks( - new SplitApksGenerator( - allModules, - bundleVersion, - apkGenerationConfiguration.setForInstantAppVariants(false).build()) - .generateSplits()); - - // Generate instant splits for any instant compatible modules. - ImmutableList instantModules = - allModules.stream().filter(BundleModule::isInstantModule).collect(toImmutableList()); - generatedApksBuilder.setInstantApks( - new SplitApksGenerator( - instantModules, - bundleVersion, - apkGenerationConfiguration - .setForInstantAppVariants(true) - // We can't enable this splitter for instant APKs, as currently they only - // support one variant. - .setEnableDexCompressionSplitter(false) - .build()) - .generateSplits()); - } - if (generateStandaloneApks) { - // Note: Universal APK is a special type of standalone, with no optimization dimensions. - ImmutableList modulesForFusing = - allModules.stream().filter(BundleModule::isIncludedInFusing).collect(toImmutableList()); - generatedApksBuilder.setStandaloneApks( - generateStandaloneApks( - modulesForFusing, - appBundle.getBundleMetadata(), - tempDir, - apkOptimizations, - bundleVersion)); - } // Populate alternative targeting based on variant targeting of all APKs. GeneratedApks generatedApks = AlternativeVariantTargetingPopulator.populateAlternativeVariantTargeting( @@ -194,6 +211,13 @@ public Path execute(Path tempDir) { SplitsXmlInjector splitsXmlInjector = new SplitsXmlInjector(); generatedApks = splitsXmlInjector.process(generatedApks); + ApkSetBuilder apkSetBuilder = + createApkSetBuilder( + aapt2Command, + command.getSigningConfiguration(), + bundleConfig.getCompression(), + tempDir); + // Create variants and serialize APKs. ApkSerializerManager apkSerializerManager = new ApkSerializerManager( @@ -207,10 +231,9 @@ public Path execute(Path tempDir) { if (deviceSpec.isPresent()) { allVariantsWithTargeting = apkSerializerManager.serializeApksForDevice(generatedApks, deviceSpec.get()); - } else if (command.getGenerateOnlyUniversalApk()) { - allVariantsWithTargeting = apkSerializerManager.serializeUniversalApk(generatedApks); } else { - allVariantsWithTargeting = apkSerializerManager.serializeApks(generatedApks); + allVariantsWithTargeting = + apkSerializerManager.serializeApks(generatedApks, command.getApkBuildMode()); } // Finalize the output archive. apkSetBuilder.setTableOfContentsFile( @@ -238,6 +261,10 @@ public Path execute(Path tempDir) { return command.getOutputFile(); } + private void printWarning(String message) { + command.getOutputPrintStream().ifPresent(out -> out.println("WARNING: " + message)); + } + private DeviceSpec getDeviceSpec() { AdbServer adbServer = command.getAdbServer().get(); adbServer.init(command.getAdbPath().get()); @@ -265,6 +292,33 @@ private ApkSetBuilder createApkSetBuilder( splitApkSerializer, standaloneApkSerializer, tempDir); } + private ApkGenerationConfiguration.Builder getApkGenerationConfigurationBuilder( + AppBundle appBundle, BundleConfig bundleConfig, Version bundleToolVersion) { + + ApkOptimizations apkOptimizations = getApkOptimizations(bundleConfig); + + ApkGenerationConfiguration.Builder apkGenerationConfiguration = + ApkGenerationConfiguration.builder() + .setOptimizationDimensions(apkOptimizations.getSplitDimensions()); + boolean enableNativeLibraryCompressionSplitter = + apkOptimizations.getUncompressNativeLibraries(); + apkGenerationConfiguration.setEnableNativeLibraryCompressionSplitter( + enableNativeLibraryCompressionSplitter); + if (appBundle.has32BitRenderscriptCode()) { + apkGenerationConfiguration.setInclude64BitLibs(false); + } + return apkGenerationConfiguration; + } + + private ImmutableList modulesToFuse(ImmutableList modules) { + return modules.stream().filter(BundleModule::isIncludedInFusing).collect(toImmutableList()); + } + + private ApkOptimizations getApkOptimizations(BundleConfig bundleConfig) { + return new OptimizationsMerger() + .mergeWithDefaults(bundleConfig, command.getOptimizationDimensions()); + } + private static Aapt2Command extractAapt2FromJar(Path tempDir) { return new SdkToolsLocator() .extractAapt2(tempDir) @@ -297,28 +351,6 @@ private void validateInput() { } } - private ImmutableList generateStandaloneApks( - ImmutableList modules, - BundleMetadata bundleMetadata, - Path tempDir, - ApkOptimizations apkOptimizations, - Version bundleVersion) { - - ImmutableList standaloneApks = - new BundleSharder(tempDir, bundleVersion) - .shardBundle(modules, apkOptimizations.getSplitDimensions(), bundleMetadata); - - return standaloneApks.stream() - .map( - moduleSplit -> - moduleSplit - .toBuilder() - .setVariantTargeting(standaloneApkVariantTargeting(moduleSplit)) - .setSplitType(SplitType.STANDALONE) - .build()) - .collect(toImmutableList()); - } - private static boolean targetsOnlyPreL(AppBundle bundle) { Optional maxSdkVersion = bundle.getBaseModule().getAndroidManifest().getMaxSdkVersion(); @@ -329,25 +361,4 @@ private static boolean targetsPreL(AppBundle bundle) { int baseMinSdkVersion = bundle.getBaseModule().getAndroidManifest().getEffectiveMinSdkVersion(); return baseMinSdkVersion < Versions.ANDROID_L_API_VERSION; } - - private static VariantTargeting standaloneApkVariantTargeting(ModuleSplit standaloneApk) { - ApkTargeting apkTargeting = standaloneApk.getApkTargeting(); - - VariantTargeting.Builder variantTargeting = VariantTargeting.newBuilder(); - if (apkTargeting.hasAbiTargeting()) { - variantTargeting.setAbiTargeting(apkTargeting.getAbiTargeting()); - } - if (apkTargeting.hasScreenDensityTargeting()) { - variantTargeting.setScreenDensityTargeting(apkTargeting.getScreenDensityTargeting()); - } - variantTargeting.setSdkVersionTargeting(sdkVersionTargeting(standaloneApk)); - - return variantTargeting.build(); - } - - private static SdkVersionTargeting sdkVersionTargeting(ModuleSplit moduleSplit) { - return SdkVersionTargeting.newBuilder() - .addValue(sdkVersionFrom(moduleSplit.getAndroidManifest().getEffectiveMinSdkVersion())) - .build(); - } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java index 520d2dbf..f813b663 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java @@ -233,7 +233,7 @@ public static CommandHelp help() { .setCommandDescription( CommandDescription.builder() .setShortDescription( - "Computes the over-the-wire min and max sizes of APKs served to different " + "Computes the min and max download sizes of APKs served to different " + "devices configurations from an APK Set.") .addAdditionalParagraph("The output is in CSV format.") .build()) diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java index f98f6924..326bf1fd 100755 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -224,8 +224,7 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(DEVICE_ID_FLAG.getName()) - .setExampleValue("allow-downgrade") + .setFlagName(ALLOW_DOWNGRADE_FLAG.getName()) .setOptional(true) .setDescription( "If set, allows APKs to be installed on the device even if the app is already " diff --git a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java index 328f8db8..2b02d7af 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java @@ -68,17 +68,20 @@ public ApkMatcher( "Set of requested split modules cannot be empty."); SdkVersionMatcher sdkVersionMatcher = new SdkVersionMatcher(deviceSpec); AbiMatcher abiMatcher = new AbiMatcher(deviceSpec); + MultiAbiMatcher multiAbiMatcher = new MultiAbiMatcher(deviceSpec); ScreenDensityMatcher screenDensityMatcher = new ScreenDensityMatcher(deviceSpec); LanguageMatcher languageMatcher = new LanguageMatcher(deviceSpec); DeviceFeatureMatcher deviceFeatureMatcher = new DeviceFeatureMatcher(deviceSpec); this.apkMatchers = - ImmutableList.of(sdkVersionMatcher, abiMatcher, screenDensityMatcher, languageMatcher); + ImmutableList.of( + sdkVersionMatcher, abiMatcher, multiAbiMatcher, screenDensityMatcher, languageMatcher); this.requestedModuleNames = requestedModuleNames; this.matchInstant = matchInstant; this.moduleMatcher = new ModuleMatcher(sdkVersionMatcher, deviceFeatureMatcher); this.variantMatcher = - new VariantMatcher(sdkVersionMatcher, abiMatcher, screenDensityMatcher, matchInstant); + new VariantMatcher( + sdkVersionMatcher, abiMatcher, multiAbiMatcher, screenDensityMatcher, matchInstant); } /** diff --git a/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java new file mode 100755 index 00000000..b92d6638 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java @@ -0,0 +1,132 @@ +/* + * 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.device; + +import static com.android.tools.build.bundletool.targeting.TargetingComparators.MULTI_ABI_ALIAS_COMPARATOR; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.Abi.AbiAlias; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.MultiAbi; +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.AbiName; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; + +/** A {@link TargetingDimensionMatcher} that provides matching on multiple ABI architectures. */ +public class MultiAbiMatcher extends TargetingDimensionMatcher { + + public MultiAbiMatcher(DeviceSpec deviceSpec) { + super(deviceSpec); + } + + @Override + public boolean matchesTargeting(MultiAbiTargeting targeting) { + // Test if the targeting has anything specific for multi ABI. + if (targeting.equals(MultiAbiTargeting.getDefaultInstance())) { + return true; + } + + ImmutableSet> valuesSet = + targeting.getValueList().stream() + .map(MultiAbiMatcher::abiAliases) + .collect(toImmutableSet()); + + ImmutableSet deviceAbis = deviceAbiAliases(); + + if (valuesSet.stream().noneMatch(deviceAbis::containsAll)) { + return false; + } + + ImmutableSet> alternativesSet = + targeting.getAlternativesList().stream() + .map(MultiAbiMatcher::abiAliases) + .collect(toImmutableSet()); + + // There is a match only if there is no better alternative. A better alternative is contained + // in the device's supported ABIs and is "greater" than all current targeting's values. + return alternativesSet.stream() + .noneMatch( + alternative -> + deviceAbis.containsAll(alternative) + && valuesSet.stream() + .allMatch( + value -> MULTI_ABI_ALIAS_COMPARATOR.compare(alternative, value) > 0)); + } + + @Override + protected void checkDeviceCompatibleInternal(MultiAbiTargeting targeting) { + if (targeting.equals(MultiAbiTargeting.getDefaultInstance())) { + return; + } + + ImmutableSet> valuesAndAlternativesSet = + Streams.concat( + targeting.getValueList().stream().map(MultiAbiMatcher::abiAliases), + targeting.getAlternativesList().stream().map(MultiAbiMatcher::abiAliases)) + .collect(toImmutableSet()); + + ImmutableSet deviceAbis = deviceAbiAliases(); + + if (valuesAndAlternativesSet.stream().noneMatch(deviceAbis::containsAll)) { + throw CommandExecutionException.builder() + .withMessage( + "No set of ABI architectures that the app supports is contained in the ABI " + + "architecture set of the device. Device ABIs: %s, app ABIs: %s.", + deviceAbis, valuesAndAlternativesSet) + .build(); + } + } + + @Override + protected MultiAbiTargeting getTargetingValue(ApkTargeting apkTargeting) { + return apkTargeting.getMultiAbiTargeting(); + } + + @Override + protected MultiAbiTargeting getTargetingValue(VariantTargeting variantTargeting) { + return variantTargeting.getMultiAbiTargeting(); + } + + @Override + protected boolean isDeviceDimensionPresent() { + return !getDeviceSpec().getSupportedAbisList().isEmpty(); + } + + private static ImmutableSet abiAliases(MultiAbi multiAbi) { + return multiAbi.getAbiList().stream().map(Abi::getAlias).collect(toImmutableSet()); + } + + private ImmutableSet deviceAbiAliases() { + return getDeviceSpec().getSupportedAbisList().stream() + .map( + abi -> + AbiName.fromPlatformName(abi) + .orElseThrow( + () -> + ValidationException.builder() + .withMessage("Unrecognized ABI '%s' in device spec.", abi) + .build()) + .toProto()) + .collect(toImmutableSet()); + } +} 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 ffd8d212..a6d71147 100755 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantMatcher.java @@ -47,6 +47,7 @@ public VariantMatcher(DeviceSpec deviceSpec, boolean matchInstant) { this( new SdkVersionMatcher(deviceSpec), new AbiMatcher(deviceSpec), + new MultiAbiMatcher(deviceSpec), new ScreenDensityMatcher(deviceSpec), matchInstant); } @@ -54,9 +55,11 @@ public VariantMatcher(DeviceSpec deviceSpec, boolean matchInstant) { VariantMatcher( SdkVersionMatcher sdkVersionMatcher, AbiMatcher abiMatcher, + MultiAbiMatcher multiAbiMatcher, ScreenDensityMatcher screenDensityMatcher, boolean matchInstant) { - this.variantMatchers = ImmutableList.of(sdkVersionMatcher, abiMatcher, screenDensityMatcher); + this.variantMatchers = + ImmutableList.of(sdkVersionMatcher, abiMatcher, multiAbiMatcher, screenDensityMatcher); this.matchInstant = matchInstant; } diff --git a/src/main/java/com/android/tools/build/bundletool/exceptions/manifest/ManifestVersionCodeConflictException.java b/src/main/java/com/android/tools/build/bundletool/exceptions/manifest/ManifestVersionCodeConflictException.java new file mode 100755 index 00000000..a6c57af9 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/exceptions/manifest/ManifestVersionCodeConflictException.java @@ -0,0 +1,43 @@ +/* + * 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.exceptions.manifest; + +import com.android.bundle.Errors.BundleToolError; +import com.android.bundle.Errors.ManifestModulesDifferentVersionCodes; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +/** Thrown when {@link BundleModule} version codes have conflicting values. */ +public class ManifestVersionCodeConflictException extends ManifestValidationException { + + private static final Joiner COMMA_JOINER = Joiner.on(','); + + private final ImmutableList versionCodes; + + public ManifestVersionCodeConflictException(Integer... versionCodes) { + super( + "App Bundle modules should have the same version code but found [%s].", + COMMA_JOINER.join(versionCodes)); + this.versionCodes = ImmutableList.copyOf(versionCodes); + } + + @Override + protected void customizeProto(BundleToolError.Builder builder) { + builder.setManifestModulesDifferentVersionCodes( + ManifestModulesDifferentVersionCodes.newBuilder().addAllVersionCodes(versionCodes)); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java index 80587a09..669f71c4 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java @@ -21,6 +21,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -76,6 +77,10 @@ public ZipPath getApkPath(ModuleSplit moduleSplit) { directory = ZipPath.create("standalones"); apkFileName = buildName("standalone", targetingSuffix); break; + case SYSTEM: + directory = ZipPath.create("system"); + apkFileName = buildName("system", targetingSuffix); + break; default: throw new IllegalStateException("Unrecognized split type: " + moduleSplit.getSplitType()); } @@ -103,7 +108,9 @@ private synchronized ZipPath findAndClaimUnusedPath( } private static String getTargetingSuffix(ModuleSplit moduleSplit) { - return moduleSplit.isMasterSplit() && !moduleSplit.getSplitType().equals(SplitType.STANDALONE) + return moduleSplit.isMasterSplit() + && !ImmutableSet.of(SplitType.STANDALONE, SplitType.SYSTEM) + .contains(moduleSplit.getSplitType()) ? "master" : moduleSplit.getSuffix(); } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java index e84cec94..46ae4765 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java @@ -15,6 +15,7 @@ */ package com.android.tools.build.bundletool.io; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.MANIFEST_FILENAME; import static com.android.tools.build.bundletool.model.BundleModule.RESOURCES_PROTO_PATH; @@ -49,6 +50,9 @@ import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -63,6 +67,7 @@ import java.security.SignatureException; import java.util.Optional; import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; /** Serializes APKs to Proto or Binary format. */ final class ApkSerializerHelper { @@ -134,7 +139,7 @@ Path writeToZipFile(ModuleSplit split, Path outputPath) { return outputPath; } - void writeToZipFile(ModuleSplit split, Path outputPath, Path tempDir) { + private void writeToZipFile(ModuleSplit split, Path outputPath, Path tempDir) { checkFileDoesNotExist(outputPath); createParentDirectories(outputPath); @@ -179,6 +184,33 @@ void writeToZipFile(ModuleSplit split, Path outputPath, Path tempDir) { } } + Path writeCompressedApkToZipFile(ModuleSplit split, Path outputPath) { + TempFiles.withTempDirectory( + tempDir -> { + Path tempApkOutputPath = tempDir.resolve("output.apk"); + writeToZipFile(split, tempApkOutputPath, tempDir); + writeCompressedApkToZipFile(tempApkOutputPath, outputPath); + }); + return outputPath; + } + + private void writeCompressedApkToZipFile(Path apkPath, Path outputApkGzipPath) { + checkFileDoesNotExist(outputApkGzipPath); + createParentDirectories(outputApkGzipPath); + + try (FileInputStream fileInputStream = new FileInputStream(apkPath.toFile()); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(new FileOutputStream(outputApkGzipPath.toFile()))) { + ByteStreams.copy(fileInputStream, gzipOutputStream); + } catch (IOException e) { + throw new UncheckedIOException( + String.format( + "Failed to write APK file '%s' to compressed APK file '%s'.", + apkPath, outputApkGzipPath), + e); + } + } + /** * Creates a proto-APK from the {@link ModuleSplit} and stores it on disk. * @@ -292,7 +324,8 @@ private void addNonAapt2Files(ZFile zFile, ModuleSplit split) throws IOException * Transforms the entry path in the module to the final path in the module split. * *

The entries from root/, dex/, manifest/ directories will be moved to the top level of the - * split. + * split. Entries from apex/ will be moved to the top level and named "apex_payload.img". There + * should only be one such entry. */ private ZipPath toApkEntryPath(ZipPath pathInModule) { if (pathInModule.startsWith(DEX_DIRECTORY)) { @@ -310,6 +343,13 @@ private ZipPath toApkEntryPath(ZipPath pathInModule) { pathInModule); return pathInModule.subpath(1, pathInModule.getNameCount()); } + if (pathInModule.startsWith(APEX_DIRECTORY)) { + checkArgument( + pathInModule.getNameCount() >= 2, + "Only files inside the apex directory are supported but found: %s", + pathInModule); + return ZipPath.create("apex_payload.img"); + } return pathInModule; } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java index e6ec4143..c3ea3eba 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -27,6 +27,7 @@ import com.android.bundle.Commands.Variant; import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.device.ApkMatcher; import com.android.tools.build.bundletool.io.ApkSetBuilderFactory.ApkSetBuilder; import com.android.tools.build.bundletool.model.ApkListener; @@ -39,6 +40,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.VariantKey; import com.android.tools.build.bundletool.utils.ConcurrencyUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; @@ -75,24 +77,25 @@ public ApkSerializerManager( this.firstVariantNumber = firstVariantNumber; } - public ImmutableList serializeUniversalApk(GeneratedApks generatedApks) { - checkArgument( - generatedApks.getSplitApks().isEmpty() && generatedApks.getInstantApks().isEmpty(), - "Internal error: For universal APK expecting only standalone APKs."); - return serializeApks(generatedApks, /* isUniversalApk= */ true, Optional.empty()); - } - public ImmutableList serializeApksForDevice( GeneratedApks generatedApks, DeviceSpec deviceSpec) { - return serializeApks(generatedApks, /* isUniversalApk= */ false, Optional.of(deviceSpec)); + return serializeApks(generatedApks, ApkBuildMode.DEFAULT, Optional.of(deviceSpec)); } - public ImmutableList serializeApks(GeneratedApks generatedApks) { - return serializeApks(generatedApks, /* isUniversalApk= */ false, Optional.empty()); + @VisibleForTesting + ImmutableList serializeApks(GeneratedApks generatedApks) { + return serializeApks(generatedApks, ApkBuildMode.DEFAULT); + } + + public ImmutableList serializeApks( + GeneratedApks generatedApks, ApkBuildMode apkBuildMode) { + return serializeApks(generatedApks, apkBuildMode, Optional.empty()); } private ImmutableList serializeApks( - GeneratedApks generatedApks, boolean isUniversalApk, Optional deviceSpec) { + GeneratedApks generatedApks, ApkBuildMode apkBuildMode, Optional deviceSpec) { + validateInput(generatedApks, apkBuildMode); + Predicate deviceFilter = deviceSpec.isPresent() ? new ApkMatcher(deviceSpec.get())::matchesModuleSplitByTargeting @@ -110,7 +113,7 @@ private ImmutableList serializeApks( // 1. Remove APKs not matching the device spec. // 2. Modify the APKs based on the ApkModifier. // 3. Serialize all APKs in parallel. - ApkSerializer apkSerializer = new ApkSerializer(apkListener, isUniversalApk); + ApkSerializer apkSerializer = new ApkSerializer(apkListener, apkBuildMode); // Modifies the APK using APK modifier, then returns a map by extracting the variant // of APK first and later clearing out its variant targeting. @@ -128,7 +131,9 @@ private ImmutableList serializeApks( // After variant targeting of APKs are cleared, there might be duplicate APKs // which are removed and the distinct APKs are then serialized in parallel. - ImmutableMap apkDescriptionBySplit = + // Note: Only serializing compressed system APK produces multiple ApkDescriptions, + // i.e compressed and stub APK descriptions. + ImmutableMap> apkDescriptionBySplit = finalSplitsByVariant.values().stream() .distinct() .collect( @@ -155,10 +160,8 @@ private ImmutableList serializeApks( ApkSet.newBuilder() .setModuleMetadata(appBundle.getModule(moduleName).getModuleMetadata()) .addAllApkDescription( - splitsByModuleName - .get(moduleName) - .stream() - .map(apkDescriptionBySplit::get) + splitsByModuleName.get(moduleName).stream() + .flatMap(split -> apkDescriptionBySplit.get(split).stream()) .collect(toImmutableList()))); } variants.add(variant.build()); @@ -167,6 +170,31 @@ private ImmutableList serializeApks( return variants.build(); } + private void validateInput(GeneratedApks generatedApks, ApkBuildMode apkBuildMode) { + switch (apkBuildMode) { + case DEFAULT: + checkArgument( + generatedApks.getSystemApks().isEmpty(), + "Internal error: System APKs can only be set in system mode."); + break; + case UNIVERSAL: + checkArgument( + generatedApks.getSplitApks().isEmpty() + && generatedApks.getInstantApks().isEmpty() + && generatedApks.getSystemApks().isEmpty(), + "Internal error: For universal APK expecting only standalone APKs."); + break; + case SYSTEM_COMPRESSED: + case SYSTEM: + checkArgument( + generatedApks.getSplitApks().isEmpty() + && generatedApks.getInstantApks().isEmpty() + && generatedApks.getStandaloneApks().isEmpty(), + "Internal error: For system mode expecting only system APKs."); + break; + } + } + private ModuleSplit modifyApk(ModuleSplit moduleSplit, int variantNumber) { ApkModifier.ApkDescription apkDescription = ApkModifier.ApkDescription.builder() @@ -196,35 +224,41 @@ private static ModuleSplit clearVariantTargeting(ModuleSplit moduleSplit) { private final class ApkSerializer { private final ApkListener apkListener; - private final boolean isUniversalApk; + private final ApkBuildMode apkBuildMode; - public ApkSerializer(ApkListener apkListener, boolean isUniversalApk) { + public ApkSerializer(ApkListener apkListener, ApkBuildMode apkBuildMode) { this.apkListener = apkListener; - this.isUniversalApk = isUniversalApk; + this.apkBuildMode = apkBuildMode; } - public ApkDescription serialize(ModuleSplit split) { - ApkDescription apkDescription; + public ImmutableList serialize(ModuleSplit split) { + ImmutableList apkDescriptions; switch (split.getSplitType()) { case INSTANT: - apkDescription = apkSetBuilder.addInstantApk(split); + apkDescriptions = ImmutableList.of(apkSetBuilder.addInstantApk(split)); break; case SPLIT: - apkDescription = apkSetBuilder.addSplitApk(split); + apkDescriptions = ImmutableList.of(apkSetBuilder.addSplitApk(split)); + break; + case SYSTEM: + apkDescriptions = + apkBuildMode.equals(ApkBuildMode.SYSTEM_COMPRESSED) + ? apkSetBuilder.addCompressedSystemApks(split) + : ImmutableList.of(apkSetBuilder.addSystemApk(split)); break; case STANDALONE: - apkDescription = - isUniversalApk - ? apkSetBuilder.addStandaloneUniversalApk(split) - : apkSetBuilder.addStandaloneApk(split); + apkDescriptions = + apkBuildMode.equals(ApkBuildMode.UNIVERSAL) + ? ImmutableList.of(apkSetBuilder.addStandaloneUniversalApk(split)) + : ImmutableList.of(apkSetBuilder.addStandaloneApk(split)); break; default: throw new IllegalStateException("Unexpected splitType: " + split.getSplitType()); } - apkListener.onApkFinalized(apkDescription); - - return apkDescription; + // Notify apk listener. + apkDescriptions.forEach(apkListener::onApkFinalized); + return apkDescriptions; } } } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java index e86442c8..b83be29e 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSetBuilderFactory.java @@ -25,6 +25,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.utils.files.BufferedIo; +import com.google.common.collect.ImmutableList; import com.google.protobuf.Message; import java.io.FileNotFoundException; import java.io.IOException; @@ -49,6 +50,15 @@ public interface ApkSetBuilder { /** Adds an instant split APK to the APK Set archive. */ ApkDescription addInstantApk(ModuleSplit split); + /** Adds an system APK to the APK Set archive. */ + ApkDescription addSystemApk(ModuleSplit split); + + /** + * Adds an compressed system APK and an and an additional uncompressed stub APK containing just + * the android manifest to the APK Set archive. + */ + ImmutableList addCompressedSystemApks(ModuleSplit split); + /** Sets the TOC file in the APK Set archive. */ void setTableOfContentsFile(BuildApksResult tableOfContentsProto); @@ -90,7 +100,7 @@ public ApkSetArchiveBuilder( @Override public ApkDescription addSplitApk(ModuleSplit split) { ApkDescription apkDescription = splitApkSerializer.writeSplitToDisk(split, tempDirectory); - addToApkSetArchive(apkDescription); + addToApkSetArchive(apkDescription.getPath()); return apkDescription; } @@ -98,14 +108,14 @@ public ApkDescription addSplitApk(ModuleSplit split) { public ApkDescription addInstantApk(ModuleSplit split) { ApkDescription apkDescription = splitApkSerializer.writeInstantSplitToDisk(split, tempDirectory); - addToApkSetArchive(apkDescription); + addToApkSetArchive(apkDescription.getPath()); return apkDescription; } @Override public ApkDescription addStandaloneApk(ModuleSplit split) { ApkDescription apkDescription = standaloneApkSerializer.writeToDisk(split, tempDirectory); - addToApkSetArchive(apkDescription); + addToApkSetArchive(apkDescription.getPath()); return apkDescription; } @@ -113,15 +123,31 @@ public ApkDescription addStandaloneApk(ModuleSplit split) { public ApkDescription addStandaloneUniversalApk(ModuleSplit split) { ApkDescription apkDescription = standaloneApkSerializer.writeToDiskAsUniversal(split, tempDirectory); - addToApkSetArchive(apkDescription); + addToApkSetArchive(apkDescription.getPath()); + return apkDescription; + } + + @Override + public ApkDescription addSystemApk(ModuleSplit split) { + ApkDescription apkDescription = + standaloneApkSerializer.writeSystemApkToDisk(split, tempDirectory); + addToApkSetArchive(apkDescription.getPath()); return apkDescription; } - private void addToApkSetArchive(ApkDescription apkDescription) { - Path apkPath = tempDirectory.resolve(apkDescription.getPath()); - checkFileExistsAndReadable(apkPath); + @Override + public ImmutableList addCompressedSystemApks(ModuleSplit split) { + ImmutableList apkDescriptions = + standaloneApkSerializer.writeCompressedSystemApksToDisk(split, tempDirectory); + apkDescriptions.forEach(apkDescription -> addToApkSetArchive(apkDescription.getPath())); + return apkDescriptions; + } + + private void addToApkSetArchive(String relativeApkPath) { + Path fullApkPath = tempDirectory.resolve(relativeApkPath); + checkFileExistsAndReadable(fullApkPath); apkSetZipBuilder.addFileFromDisk( - ZipPath.create(apkDescription.getPath()), apkPath.toFile(), EntryOption.UNCOMPRESSED); + ZipPath.create(relativeApkPath), fullApkPath.toFile(), EntryOption.UNCOMPRESSED); } @Override @@ -177,6 +203,16 @@ public ApkDescription addStandaloneUniversalApk(ModuleSplit split) { return standaloneApkSerializer.writeToDiskAsUniversal(split, outputDirectory); } + @Override + public ApkDescription addSystemApk(ModuleSplit split) { + return standaloneApkSerializer.writeSystemApkToDisk(split, outputDirectory); + } + + @Override + public ImmutableList addCompressedSystemApks(ModuleSplit split) { + return standaloneApkSerializer.writeCompressedSystemApksToDisk(split, outputDirectory); + } + @Override public void setTableOfContentsFile(BuildApksResult tableOfContentsProto) { writeProtoFile(tableOfContentsProto, outputDirectory.resolve("toc.pb")); diff --git a/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java index 7b97b5fc..d90d3a0b 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/AppBundleSerializer.java @@ -38,10 +38,13 @@ public void writeToDisk(AppBundle bundle, Path pathOnDisk) throws IOException { zipBuilder.addFileWithProtoContent( ZipPath.create(BUNDLE_CONFIG_FILE_NAME), bundle.getBundleConfig()); - for (Entry metadataEntry : - bundle.getBundleMetadata().getFileDataMap().entrySet()) { - zipBuilder.addFile( - METADATA_DIRECTORY.resolve(metadataEntry.getKey()), metadataEntry.getValue()); + // APEX bundles do not have metadata files. + if (bundle.getModules().isEmpty() || !bundle.getBaseModule().getApexConfig().isPresent()) { + for (Entry metadataEntry : + bundle.getBundleMetadata().getFileDataMap().entrySet()) { + zipBuilder.addFile( + METADATA_DIRECTORY.resolve(metadataEntry.getKey()), metadataEntry.getValue()); + } } for (BundleModule module : bundle.getModules().values()) { @@ -78,6 +81,12 @@ public void writeToDisk(AppBundle bundle, Path pathOnDisk) throws IOException { resourceTable -> zipBuilder.addFileWithProtoContent( moduleDir.resolve(BundleModule.RESOURCES_PROTO_PATH), resourceTable)); + module + .getApexConfig() + .ifPresent( + apexConfig -> + zipBuilder.addFileWithProtoContent( + moduleDir.resolve(BundleModule.APEX_PROTO_PATH), apexConfig)); } zipBuilder.writeTo(pathOnDisk); diff --git a/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java b/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java index 58e7510f..8bcde9b7 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java +++ b/src/main/java/com/android/tools/build/bundletool/io/StandaloneApkSerializer.java @@ -16,22 +16,25 @@ package com.android.tools.build.bundletool.io; +import static com.google.common.base.Preconditions.checkArgument; + import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.StandaloneApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata.SystemApkType; import com.android.bundle.Config.Compression; import com.android.tools.build.bundletool.model.Aapt2Command; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import java.nio.file.Path; import java.util.Optional; /** Serializes standalone APKs to disk. */ public class StandaloneApkSerializer { - public static final String STANDALONE_APKS_SUB_DIR = "standalones"; - private final ApkPathManager apkPathManager; private final ApkSerializerHelper apkSerializerHelper; @@ -53,6 +56,27 @@ public ApkDescription writeToDiskAsUniversal(ModuleSplit standaloneSplit, Path o return writeToDiskInternal(standaloneSplit, outputDirectory, ZipPath.create("universal.apk")); } + public ApkDescription writeSystemApkToDisk(ModuleSplit systemSplit, Path outputDirectory) { + return writeSystemApkToDiskInternal(systemSplit, outputDirectory, SystemApkType.SYSTEM); + } + /** + * Writes an compressed system APK and stub system APK containing just android manifest to disk. + */ + public ImmutableList writeCompressedSystemApksToDisk( + ModuleSplit systemSplit, Path outputDirectory) { + ApkDescription stubApkDescription = + writeSystemApkToDiskInternal( + splitWithOnlyManifest(systemSplit), outputDirectory, SystemApkType.SYSTEM_STUB); + ZipPath compressedApkPath = + ZipPath.create(getCompressedApkPathFromStubApkPath(stubApkDescription.getPath())); + apkSerializerHelper.writeCompressedApkToZipFile( + systemSplit, outputDirectory.resolve(compressedApkPath.toString())); + return ImmutableList.of( + stubApkDescription, + createSystemApkDescription( + systemSplit, compressedApkPath, SystemApkType.SYSTEM_COMPRESSED)); + } + @VisibleForTesting ApkDescription writeToDiskInternal( ModuleSplit standaloneSplit, Path outputDirectory, ZipPath apkPath) { @@ -67,4 +91,43 @@ ApkDescription writeToDiskInternal( .setTargeting(standaloneSplit.getApkTargeting()) .build(); } + + private ApkDescription writeSystemApkToDiskInternal( + ModuleSplit systemSplit, Path outputDirectory, SystemApkMetadata.SystemApkType apkType) { + ZipPath apkPath = apkPathManager.getApkPath(systemSplit); + apkSerializerHelper.writeToZipFile(systemSplit, outputDirectory.resolve(apkPath.toString())); + return createSystemApkDescription(systemSplit, apkPath, apkType); + } + + /** + * The compressed system APK should have the same file name as stub system APK (".apk" file + * extension) but end with ".apk.gz" file extension. + */ + private static String getCompressedApkPathFromStubApkPath(String stubApkPath) { + checkArgument(stubApkPath.endsWith(".apk")); + return stubApkPath + ".gz"; + } + + private static ApkDescription createSystemApkDescription( + ModuleSplit systemSplit, ZipPath apkPath, SystemApkMetadata.SystemApkType apkType) { + return ApkDescription.newBuilder() + .setPath(apkPath.toString()) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .addAllFusedModuleName(systemSplit.getAndroidManifest().getFusedModuleNames()) + .setSystemApkType(apkType)) + .setTargeting(systemSplit.getApkTargeting()) + .build(); + } + + private static ModuleSplit splitWithOnlyManifest(ModuleSplit split) { + return ModuleSplit.builder() + .setModuleName(split.getModuleName()) + .setSplitType(split.getSplitType()) + .setVariantTargeting(split.getVariantTargeting()) + .setApkTargeting(split.getApkTargeting()) + .setAndroidManifest(split.getAndroidManifest()) + .setMasterSplit(split.isMasterSplit()) + .build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java b/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java index 7a3119ed..065d009d 100755 --- a/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ZipBuilder.java @@ -48,6 +48,7 @@ * invoked. */ public final class ZipBuilder { + private static final Long EPOCH = 0L; /** Entries to be output. */ private final Map entries = new LinkedHashMap<>(); @@ -73,11 +74,13 @@ public synchronized Path writeTo(Path target) throws IOException { // For directories, we append "/" at the end of the file path since that's what the // ZipEntry class relies on. ZipEntry zipEntry = new ZipEntry(path + "/"); + zipEntry.setTime(EPOCH); outZip.putNextEntry(zipEntry); // Directories are represented as having empty content in a zip file, so we don't write // any bytes to the outZip for this entry. } else { ZipEntry zipEntry = new ZipEntry(path.toString()); + zipEntry.setTime(EPOCH); if (entry.hasOption(EntryOption.UNCOMPRESSED)) { zipEntry.setMethod(ZipEntry.STORED); // ZipFile API requires us to set the following properties manually for uncompressed diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java b/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java index 53bbe1b3..ad59a614 100755 --- a/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMerger.java @@ -69,7 +69,6 @@ public class ModuleSplitsToShardMerger { private static final BundleModuleName BASE_MODULE_NAME = BundleModuleName.create(BundleModuleName.BASE_MODULE_NAME); - private static final BundleModuleName SHARD_MODULE_NAME = BundleModuleName.create("base"); private final DexMerger dexMerger; private final Path globalTempDir; @@ -79,7 +78,14 @@ public ModuleSplitsToShardMerger(DexMerger dexMerger, Path globalTempDir) { this.globalTempDir = globalTempDir; } - /** Merges each collection of splits into a single standalone APK (aka shard). */ + /** + * Gets a list of collections of splits, and merges each collection into a single standalone APK + * (aka shard). + * + * @param unfusedShards a list of lists - each inner list is a collection of splits + * @param bundleMetadata the App Bundle metadata + * @return a list of shards, each one made of the corresponding collection of splits + */ public ImmutableList merge( ImmutableList> unfusedShards, BundleMetadata bundleMetadata) { // Results of the dex merging are cached. Due to the nature of the cache keys and values, the @@ -144,21 +150,72 @@ ModuleSplit mergeSingleShard( mergedAndroidManifest.toEditor().setFusedModuleNames(fusedModuleNames).save(); // Construct the final shard. + return buildShard( + mergedEntriesByPath.values(), + mergedDexFiles, + mergedSplitTargeting, + finalAndroidManifest, + mergedResourceTable); + } + + /** + * Gets a list of collections of splits, and merges each collection into a single standalone APK + * (aka shard). + * + * @param unfusedShards a list of lists - each inner list is a collection of splits + * @return a list of shards, each one made of the corresponding collection of splits + */ + public ImmutableList mergeApex( + ImmutableList> unfusedShards) { + return unfusedShards.stream().map(this::mergeSingleApexShard).collect(toImmutableList()); + } + + @VisibleForTesting + ModuleSplit mergeSingleApexShard(ImmutableList splitsOfShard) { + checkState(!splitsOfShard.isEmpty(), "A shard is made of at least one split."); + + Map mergedEntriesByPath = new HashMap<>(); + ApkTargeting splitTargeting = ApkTargeting.getDefaultInstance(); + + for (ModuleSplit split : splitsOfShard) { + // An APEX shard is made of one master split and one multi-Abi split, so we use the latter. + splitTargeting = + splitTargeting.hasMultiAbiTargeting() ? splitTargeting : split.getApkTargeting(); + + for (ModuleEntry entry : split.getEntries()) { + mergeEntries(mergedEntriesByPath, split, entry); + } + } + + // Construct the final shard. + return buildShard( + mergedEntriesByPath.values(), + ImmutableList.of(), + splitTargeting, + // An APEX module is made of one module, so any manifest works. + splitsOfShard.get(0).getAndroidManifest(), + Optional.empty()); + } + + private ModuleSplit buildShard( + Collection entriesByPath, + Collection mergedDexFiles, + ApkTargeting splitTargeting, + AndroidManifest androidManifest, + Optional mergedResourceTable) { + ImmutableList entries = + ImmutableList.builder().addAll(entriesByPath).addAll(mergedDexFiles).build(); ModuleSplit.Builder shard = ModuleSplit.builder() - .setAndroidManifest(finalAndroidManifest) - .setEntries( - ImmutableList.builder() - .addAll(mergedEntriesByPath.values()) - .addAll(mergedDexFiles) - .build()) - .setApkTargeting(mergedSplitTargeting) + .setAndroidManifest(androidManifest) + .setEntries(entries) + .setApkTargeting(splitTargeting) .setSplitType(SplitType.STANDALONE) // We don't care about the following properties for shards. The values are set just to // satisfy contract of @AutoValue.Builder. // `nativeConfig` is optional and therefore not being set. .setMasterSplit(false) - .setModuleName(SHARD_MODULE_NAME) + .setModuleName(BASE_MODULE_NAME) .setVariantTargeting(VariantTargeting.getDefaultInstance()); mergedResourceTable.ifPresent(shard::setResourceTable); return shard.build(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java index f3467532..a92875c8 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -104,6 +104,10 @@ public BundleModule getModule(BundleModuleName moduleName) { return module; } + public boolean has32BitRenderscriptCode() { + return getModules().values().stream().anyMatch(BundleModule::hasRenderscript32Bitcode); + } + public BundleConfig getBundleConfig() { return bundleConfig; } diff --git a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java index 23cd4479..0d8ed14b 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java +++ b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java @@ -72,6 +72,9 @@ public abstract class BundleModule { /** The top-level file of an App Bundle module that contains APEX targeting configuration. */ public static final ZipPath APEX_PROTO_PATH = ZipPath.create("apex.pb"); + /** The file of an App Bundle module that contains the APEX manifest. */ + public static final ZipPath APEX_MANIFEST_PATH = ZipPath.create("root/apex_manifest.json"); + /** Used to parse file names in the apex/ directory, for multi-Abi targeting. */ public static final Splitter ABI_SPLITTER = Splitter.on(".").omitEmptyStrings(); @@ -160,6 +163,11 @@ public boolean isInstantModule() { return isInstantModule.orElse(false); } + @Memoized + public boolean hasRenderscript32Bitcode() { + return findEntries(zipPath -> zipPath.toString().endsWith(".bc")).findFirst().isPresent(); + } + public ImmutableList getDependencies() { return getAndroidManifest().getUsesSplits(); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java index 35cce7f2..af7c3626 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java +++ b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java @@ -39,12 +39,18 @@ public abstract class GeneratedApks { public abstract ImmutableList getStandaloneApks(); + public abstract ImmutableList getSystemApks(); + public int size() { - return getInstantApks().size() + getSplitApks().size() + getStandaloneApks().size(); + return getInstantApks().size() + + getSplitApks().size() + + getStandaloneApks().size() + + getSystemApks().size(); } public Stream getAllApksStream() { - return Stream.of(getStandaloneApks(), getInstantApks(), getSplitApks()).flatMap(List::stream); + return Stream.of(getStandaloneApks(), getInstantApks(), getSplitApks(), getSystemApks()) + .flatMap(List::stream); } /** Returns all apks grouped and ordered by {@link VariantKey}. */ @@ -57,7 +63,8 @@ public static Builder builder() { return new AutoValue_GeneratedApks.Builder() .setInstantApks(ImmutableList.of()) .setSplitApks(ImmutableList.of()) - .setStandaloneApks(ImmutableList.of()); + .setStandaloneApks(ImmutableList.of()) + .setSystemApks(ImmutableList.of()); } /** Creates a GeneratedApk instance from a list of module splits. */ @@ -68,18 +75,20 @@ public static GeneratedApks fromModuleSplits(ImmutableList moduleSp .setInstantApks(groups.getOrDefault(SplitType.INSTANT, ImmutableList.of())) .setSplitApks(groups.getOrDefault(SplitType.SPLIT, ImmutableList.of())) .setStandaloneApks(groups.getOrDefault(SplitType.STANDALONE, ImmutableList.of())) + .setSystemApks(groups.getOrDefault(SplitType.SYSTEM, ImmutableList.of())) .build(); } /** Builder for {@link GeneratedApks}. */ @AutoValue.Builder public abstract static class Builder { - public abstract GeneratedApks.Builder setInstantApks(ImmutableList instantApks); + public abstract Builder setInstantApks(ImmutableList instantApks); + + public abstract Builder setSplitApks(ImmutableList splitApks); - public abstract GeneratedApks.Builder setSplitApks(ImmutableList splitApks); + public abstract Builder setStandaloneApks(ImmutableList standaloneApks); - public abstract GeneratedApks.Builder setStandaloneApks( - ImmutableList standaloneApks); + public abstract Builder setSystemApks(ImmutableList systemApks); public abstract GeneratedApks build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java index 907e897a..a88e1354 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.DEX_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.LIB_DIRECTORY; @@ -28,19 +29,24 @@ import static com.google.common.collect.MoreCollectors.toOptional; import com.android.aapt.Resources.ResourceTable; +import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; +import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.GraphicsApi; import com.android.bundle.Targeting.GraphicsApiTargeting; import com.android.bundle.Targeting.LanguageTargeting; +import com.android.bundle.Targeting.MultiAbi; +import com.android.bundle.Targeting.MultiAbiTargeting; import com.android.bundle.Targeting.OpenGlVersion; import com.android.bundle.Targeting.TextureCompressionFormatTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.android.bundle.Targeting.VulkanVersion; import com.android.tools.build.bundletool.utils.ResourcesUtils; import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; @@ -55,9 +61,12 @@ @AutoValue public abstract class ModuleSplit { - /** The split type being represented by this split. It can be a standalone, split, or instant. */ + private static final Joiner MULTI_ABI_SUFFIX_JOINER = Joiner.on('.'); + + /** The split type being represented by this split. */ public enum SplitType { STANDALONE, + SYSTEM, SPLIT, INSTANT } @@ -68,7 +77,7 @@ public enum SplitType { /** Returns the targeting of the Variant this instance belongs to. */ public abstract VariantTargeting getVariantTargeting(); - /** Whether this ModuleSplit instance represents a standalone, split or instant apk. */ + /** Whether this ModuleSplit instance represents a standalone, split, instant or system apk. */ public abstract SplitType getSplitType(); /** @@ -94,6 +103,9 @@ public enum SplitType { public abstract Optional getAssetsConfig(); + /** The module APEX configuration - what system images it contains and with what targeting. */ + public abstract Optional getApexConfig(); + public abstract Builder toBuilder(); /** Returns true iff this is split of the base module. */ @@ -118,16 +130,19 @@ public String getSuffix() { AbiTargeting abiTargeting = getApkTargeting().getAbiTargeting(); if (!abiTargeting.getValueList().isEmpty()) { - abiTargeting - .getValueList() - .forEach( - value -> - suffixJoiner.add( - AbiName.fromProto(value.getAlias()).getPlatformName().replace('-', '_'))); + abiTargeting.getValueList().forEach(value -> suffixJoiner.add(formatAbi(value))); } else if (!abiTargeting.getAlternativesList().isEmpty()) { suffixJoiner.add("other_abis"); } + MultiAbiTargeting multiAbiTargeting = getApkTargeting().getMultiAbiTargeting(); + for (MultiAbi value : multiAbiTargeting.getValueList()) { + suffixJoiner.add( + MULTI_ABI_SUFFIX_JOINER.join( + value.getAbiList().stream().map(ModuleSplit::formatAbi).collect(toImmutableList()))); + } + // Alternatives without values are not supported for MultiAbiTargeting. + LanguageTargeting languageTargeting = getApkTargeting().getLanguageTargeting(); if (!languageTargeting.getValueList().isEmpty()) { languageTargeting.getValueList().forEach(suffixJoiner::add); @@ -168,6 +183,10 @@ public String getSuffix() { return suffixJoiner.toString(); } + private static String formatAbi(Abi abi) { + return AbiName.fromProto(abi.getAlias()).getPlatformName().replace('-', '_'); + } + private static String formatGraphicsApi(GraphicsApi graphicsTargeting) { StringJoiner result = new StringJoiner("_"); if (graphicsTargeting.hasMinOpenGlVersion()) { @@ -366,6 +385,18 @@ public static ModuleSplit forRoot(BundleModule bundleModule, VariantTargeting va variantTargeting); } + /** + * Creates a {@link ModuleSplit} only with the apex image entries with empty APK targeting and + * default L+ variant targeting. + */ + public static ModuleSplit forApex(BundleModule bundleModule) { + return fromBundleModule( + bundleModule, + entry -> entry.getPath().startsWith(APEX_DIRECTORY), + /* setResourceTable= */ false, + lPlusVariantTargeting()); + } + /** * Creates a {@link ModuleSplit} with entries from the Bundle Module satisfying the predicate with * a given variant targeting. @@ -393,6 +424,7 @@ private static ModuleSplit fromBundleModule( bundleModule.getNativeConfig().ifPresent(splitBuilder::setNativeConfig); bundleModule.getAssetsConfig().ifPresent(splitBuilder::setAssetsConfig); + bundleModule.getApexConfig().ifPresent(splitBuilder::setApexConfig); if (setResourceTable) { bundleModule.getResourceTable().ifPresent(splitBuilder::setResourceTable); } @@ -440,6 +472,11 @@ public abstract static class Builder { public abstract Builder setAssetsConfig(Assets assetsConfig); + /** + * Sets the module APEX configuration - what system images it contains and with what targeting. + */ + public abstract Builder setApexConfig(ApexImages apexConfig); + public abstract Builder setApkTargeting(ApkTargeting targeting); public abstract Builder setVariantTargeting(VariantTargeting targeting); diff --git a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java index 56c10dfb..b2456094 100755 --- a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java +++ b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java @@ -19,6 +19,7 @@ import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.INSTANT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SPLIT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.STANDALONE; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SYSTEM; import static com.android.tools.build.bundletool.targeting.TargetingComparators.VARIANT_TARGETING_COMPARATOR; import static java.util.Comparator.comparing; @@ -46,7 +47,9 @@ public static VariantKey create(ModuleSplit moduleSplit) { @Override public int compareTo(VariantKey o) { // Instant APKs get the lowest variant numbers followed by standalone and then split APKs. - return comparing(VariantKey::getSplitType, Ordering.explicit(INSTANT, STANDALONE, SPLIT)) + // System APKs never occur with other apk types, its ordering position doesn't matter. + return comparing( + VariantKey::getSplitType, Ordering.explicit(INSTANT, STANDALONE, SPLIT, SYSTEM)) .thenComparing(VariantKey::getVariantTargeting, VARIANT_TARGETING_COMPARATOR) .compare(this, o); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitter.java new file mode 100755 index 00000000..d4a74030 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitter.java @@ -0,0 +1,91 @@ +/* + * 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.splitters; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.function.Function.identity; + +import com.android.bundle.Files.TargetedApexImage; +import com.android.bundle.Targeting.MultiAbi; +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import java.util.List; + +/** Splits the APEX images in the module by ABI. */ +public class AbiApexImagesSplitter implements ModuleSplitSplitter { + + /** Generates {@link ModuleSplit} objects dividing the APEX images by ABI. */ + @Override + public ImmutableCollection split(ModuleSplit moduleSplit) { + if (!moduleSplit.getApexConfig().isPresent()) { + return ImmutableList.of(moduleSplit); + } + + List allTargetedImages = moduleSplit.getApexConfig().get().getImageList(); + + // A set of all MultiAbis (flattened for repeated values) for easy generation of alternatives. + ImmutableSet allTargeting = + allTargetedImages.stream() + .flatMap(image -> image.getTargeting().getMultiAbi().getValueList().stream()) + .collect(toImmutableSet()); + + // This prevents O(n^2). + ImmutableMap apexPathToEntryMap = + buildApexPathToEntryMap(allTargetedImages, moduleSplit); + + ImmutableList.Builder splits = new ImmutableList.Builder<>(); + for (TargetedApexImage targetedApexImage : allTargetedImages) { + ModuleEntry entry = apexPathToEntryMap.get(targetedApexImage.getPath()); + List targeting = targetedApexImage.getTargeting().getMultiAbi().getValueList(); + ModuleSplit.Builder splitBuilder = + moduleSplit + .toBuilder() + .setApkTargeting( + moduleSplit + .getApkTargeting() + .toBuilder() + .setMultiAbiTargeting( + MultiAbiTargeting.newBuilder() + .addAllValue(targeting) + .addAllAlternatives( + Sets.difference(allTargeting, ImmutableSet.copyOf(targeting)))) + .build()) + .setMasterSplit(false) + .setEntries(ImmutableList.of(entry)); + splits.add(splitBuilder.build()); + } + + return splits.build(); + } + + private static ImmutableMap buildApexPathToEntryMap( + List allTargetedImages, ModuleSplit moduleSplit) { + ImmutableMap pathToEntry = + Maps.uniqueIndex(moduleSplit.getEntries(), entry -> entry.getPath().toString()); + return allTargetedImages.stream() + .map(TargetedApexImage::getPath) + .collect(toImmutableMap(identity(), pathToEntry::get)); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitter.java index d056e899..2dd91b89 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitter.java @@ -22,8 +22,10 @@ import com.android.bundle.Files.TargetedNativeDirectory; import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.NativeDirectoryTargeting; +import com.android.tools.build.bundletool.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.ModuleEntry; import com.android.tools.build.bundletool.model.ModuleSplit; import com.google.common.collect.ImmutableCollection; @@ -38,6 +40,16 @@ /** Splits the native libraries in the module by ABI. */ public class AbiNativeLibrariesSplitter implements ModuleSplitSplitter { + private final boolean include64BitLibs; + + public AbiNativeLibrariesSplitter(boolean include64BitLibs) { + this.include64BitLibs = include64BitLibs; + } + + public AbiNativeLibrariesSplitter() { + this(/* include64BitLibs= */ true); + } + /** Generates {@link ModuleSplit} objects dividing the native libraries by ABI. */ @Override public ImmutableCollection split(ModuleSplit moduleSplit) { @@ -60,6 +72,18 @@ public ImmutableCollection split(ModuleSplit moduleSplit) { .map(NativeDirectoryTargeting::getAbi) .collect(toImmutableSet()); + // We need to know the exact set of ABIs that we will generate, to set alternatives correctly. + ImmutableSet abisToGenerate = + allAbis.stream().filter(abi -> include64BitLibs || !is64Bit(abi)).collect(toImmutableSet()); + + if (abisToGenerate.isEmpty() && !include64BitLibs) { + throw CommandExecutionException.builder() + .withMessage( + "Generation of 64-bit native libraries is disabled, but App Bundle contains " + + "only 64-bit native libraries.") + .build(); + } + // Any entries not claimed by the ABI splits will be returned in a separate split using the // original targeting. HashSet leftOverEntries = new HashSet<>(moduleSplit.getEntries()); @@ -71,23 +95,26 @@ public ImmutableCollection split(ModuleSplit moduleSplit) { .flatMap(directory -> moduleSplit.findEntriesUnderPath(directory.getPath())) .collect(toImmutableList()); - ModuleSplit.Builder splitBuilder = - moduleSplit - .toBuilder() - .setApkTargeting( - moduleSplit - .getApkTargeting() - .toBuilder() - .setAbiTargeting( - AbiTargeting.newBuilder() - .addValue(targeting.getAbi()) - .addAllAlternatives( - Sets.difference(allAbis, ImmutableSet.of(targeting.getAbi())))) - .build()) - .setMasterSplit(false) - .addMasterManifestMutator(withSplitsRequired(true)) - .setEntries(entriesList); - splits.add(splitBuilder.build()); + if (!is64Bit(targeting.getAbi()) || include64BitLibs) { + ModuleSplit.Builder splitBuilder = + moduleSplit + .toBuilder() + .setApkTargeting( + moduleSplit + .getApkTargeting() + .toBuilder() + .setAbiTargeting( + AbiTargeting.newBuilder() + .addValue(targeting.getAbi()) + .addAllAlternatives( + Sets.difference( + abisToGenerate, ImmutableSet.of(targeting.getAbi())))) + .build()) + .setMasterSplit(false) + .addMasterManifestMutator(withSplitsRequired(true)) + .setEntries(entriesList); + splits.add(splitBuilder.build()); + } leftOverEntries.removeAll(entriesList); } if (!leftOverEntries.isEmpty()) { @@ -95,4 +122,10 @@ public ImmutableCollection split(ModuleSplit moduleSplit) { } return splits.build(); } + + private static boolean is64Bit(Abi abi) { + return abi.getAlias().equals(AbiAlias.ARM64_V8A) + || abi.getAlias().equals(AbiAlias.X86_64) + || abi.getAlias().equals(AbiAlias.MIPS64); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java index 30e6ecea..6b57ff98 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java @@ -33,6 +33,9 @@ public abstract class ApkGenerationConfiguration { public abstract boolean getEnableDexCompressionSplitter(); + /** Whether to include 64-bit native library config splits. */ + public abstract boolean getInclude64BitLibs(); + /** * Returns a list of ABIs for placeholder libraries that should be populated for base modules * without native code. See {@link AbiPlaceholderInjector} for details. @@ -44,6 +47,7 @@ public static ApkGenerationConfiguration.Builder builder() { .setForInstantAppVariants(false) .setEnableNativeLibraryCompressionSplitter(false) .setEnableDexCompressionSplitter(false) + .setInclude64BitLibs(true) .setAbisForPlaceholderLibs(ImmutableSet.of()) .setOptimizationDimensions(ImmutableSet.of()); } @@ -68,6 +72,8 @@ public abstract Builder setEnableNativeLibraryCompressionSplitter( public abstract Builder setAbisForPlaceholderLibs(ImmutableSet abis); + public abstract Builder setInclude64BitLibs(boolean shouldGenerate); + public abstract ApkGenerationConfiguration build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java b/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java index 2f653f6f..ef049839 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/BundleSharder.java @@ -36,6 +36,7 @@ import com.android.tools.build.bundletool.version.Version; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import java.nio.file.Path; @@ -52,12 +53,18 @@ */ public class BundleSharder { - private final Path globalTempDir; private final Version bundleVersion; + private final ModuleSplitsToShardMerger merger; + private final boolean generate64BitShards; public BundleSharder(Path globalTempDir, Version bundleVersion) { - this.globalTempDir = globalTempDir; + this(globalTempDir, bundleVersion, /* generate64BitShards= */ true); + } + + public BundleSharder(Path globalTempDir, Version bundleVersion, boolean generate64BitShards) { this.bundleVersion = bundleVersion; + this.merger = new ModuleSplitsToShardMerger(new D8DexMerger(), globalTempDir); + this.generate64BitShards = generate64BitShards; } /** @@ -99,8 +106,17 @@ public ImmutableList shardBundle( ImmutableList> unfusedShards = groupSplitsToShards(moduleSplits); // Fuse each group of splits into a sharded APK. - return new ModuleSplitsToShardMerger(new D8DexMerger(), globalTempDir) - .merge(unfusedShards, bundleMetadata); + return merger.merge(unfusedShards, bundleMetadata); + } + + /** + * Generates sharded APKs from APEX module. Each sharded APK is generated by fusing one master + * split and one system image file, targeted by multi Abi. + */ + public ImmutableList shardApexBundle(BundleModule apexModule) { + ImmutableList splits = generateSplits(apexModule, ImmutableSet.of()); + ImmutableList> unfusedShards = groupSplitsToShardsForApex(splits); + return merger.mergeApex(unfusedShards); } private ImmutableList generateSplits( @@ -115,6 +131,10 @@ private ImmutableList generateSplits( SplittingPipeline resourcesPipeline = createResourcesSplittingPipeline(shardingDimensions); rawSplits.addAll(resourcesPipeline.split(ModuleSplit.forResources(module))); + // Apex images splits. + SplittingPipeline apexPipeline = createApexImagesSplittingPipeline(); + rawSplits.addAll(apexPipeline.split(ModuleSplit.forApex(module))); + // Other files. rawSplits.add(ModuleSplit.forAssets(module)); rawSplits.add(ModuleSplit.forDex(module)); @@ -136,20 +156,23 @@ private ImmutableList generateSplits( private SplittingPipeline createNativeLibrariesSplittingPipeline( ImmutableSet shardingDimensions) { - ImmutableList.Builder nativeSplitters = ImmutableList.builder(); - if (shardingDimensions.contains(OptimizationDimension.ABI)) { - nativeSplitters.add(new AbiNativeLibrariesSplitter()); - } - return SplittingPipeline.create(nativeSplitters.build()); + return SplittingPipeline.create( + shardingDimensions.contains(OptimizationDimension.ABI) + ? ImmutableList.of(new AbiNativeLibrariesSplitter(generate64BitShards)) + : ImmutableList.of()); } private SplittingPipeline createResourcesSplittingPipeline( ImmutableSet shardingDimensions) { - ImmutableList.Builder resourceSplitters = ImmutableList.builder(); - if (shardingDimensions.contains(OptimizationDimension.SCREEN_DENSITY)) { - resourceSplitters.add(new ScreenDensityResourcesSplitter(bundleVersion)); - } - return SplittingPipeline.create(resourceSplitters.build()); + return SplittingPipeline.create( + shardingDimensions.contains(OptimizationDimension.SCREEN_DENSITY) + ? ImmutableList.of(new ScreenDensityResourcesSplitter(bundleVersion)) + : ImmutableList.of()); + } + + private SplittingPipeline createApexImagesSplittingPipeline() { + // We always split APEX image files by MultiAbi, regardless of OptimizationDimension. + return SplittingPipeline.create(ImmutableList.of(new AbiApexImagesSplitter())); } private ImmutableList> groupSplitsToShards( @@ -231,6 +254,22 @@ private ImmutableList> groupSplitsToShards( return shards.build(); } + private ImmutableList> groupSplitsToShardsForApex( + ImmutableList splits) { + Set multiAbiSplits = + subsetWithTargeting(splits, ApkTargeting::hasMultiAbiTargeting); + Set masterSplits = Sets.difference(ImmutableSet.copyOf(splits), multiAbiSplits); + + ModuleSplit masterSplit = Iterables.getOnlyElement(masterSplits); + checkState( + masterSplit.getApkTargeting().equals(ApkTargeting.getDefaultInstance()), + "Master splits are expected to have default targeting."); + + return multiAbiSplits.stream() + .map(abiSplit -> ImmutableList.of(masterSplit, abiSplit)) + .collect(toImmutableList()); + } + private static ImmutableSet subsetWithTargeting( ImmutableList splits, Predicate predicate) { return splits diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java index f6b5bdda..8ea9d870 100755 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java @@ -240,7 +240,8 @@ private SplittingPipeline createNativeLibrariesSplittingPipeline() { if (apkGenerationConfiguration .getOptimizationDimensions() .contains(OptimizationDimension.ABI)) { - nativeSplitters.add(new AbiNativeLibrariesSplitter()); + nativeSplitters.add( + new AbiNativeLibrariesSplitter(apkGenerationConfiguration.getInclude64BitLibs())); } return SplittingPipeline.create(nativeSplitters.build()); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ShardedApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/ShardedApksGenerator.java new file mode 100755 index 00000000..4e9eef5c --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ShardedApksGenerator.java @@ -0,0 +1,116 @@ +/* + * 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.splitters; + +import static com.android.tools.build.bundletool.utils.TargetingProtoUtils.sdkVersionFrom; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.android.tools.build.bundletool.version.Version; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; + +/** + * Generates APKs sharded by ABI and screen density. + * + *

Supports generation of both standalone and system APKs. + */ +public final class ShardedApksGenerator { + + private final Path tempDir; + private final Version bundleVersion; + private final SplitType splitType; + private final boolean generate64BitShards; + + public ShardedApksGenerator(Path tempDir, Version bundleVersion) { + this(tempDir, bundleVersion, SplitType.STANDALONE, /* generate64BitShards= */ true); + } + + public ShardedApksGenerator( + Path tempDir, Version bundleVersion, SplitType splitType, boolean generate64BitShards) { + this.tempDir = tempDir; + this.bundleVersion = bundleVersion; + this.splitType = splitType; + this.generate64BitShards = generate64BitShards; + } + + public ImmutableList generateSplits( + ImmutableList modules, + BundleMetadata bundleMetadata, + ApkOptimizations apkOptimizations) { + + BundleSharder bundleSharder = new BundleSharder(tempDir, bundleVersion, generate64BitShards); + ImmutableList shardedApks = + bundleSharder.shardBundle(modules, apkOptimizations.getSplitDimensions(), bundleMetadata); + + return setVariantTargetingAndSplitType(shardedApks, splitType); + } + + public ImmutableList generateApexSplits(ImmutableList modules) { + + BundleSharder bundleSharder = new BundleSharder(tempDir, bundleVersion, generate64BitShards); + ImmutableList shardedApexApks = + bundleSharder.shardApexBundle(getOnlyElement(modules)); + + return setVariantTargetingAndSplitType(shardedApexApks, splitType); + } + + private static ImmutableList setVariantTargetingAndSplitType( + ImmutableList standaloneApks, SplitType splitType) { + return standaloneApks.stream() + .map( + moduleSplit -> + moduleSplit + .toBuilder() + .setVariantTargeting(standaloneApkVariantTargeting(moduleSplit)) + .setSplitType(splitType) + .build()) + .collect(toImmutableList()); + } + + private static VariantTargeting standaloneApkVariantTargeting(ModuleSplit standaloneApk) { + ApkTargeting apkTargeting = standaloneApk.getApkTargeting(); + + VariantTargeting.Builder variantTargeting = VariantTargeting.newBuilder(); + if (apkTargeting.hasAbiTargeting()) { + variantTargeting.setAbiTargeting(apkTargeting.getAbiTargeting()); + } + if (apkTargeting.hasScreenDensityTargeting()) { + variantTargeting.setScreenDensityTargeting(apkTargeting.getScreenDensityTargeting()); + } + if (apkTargeting.hasMultiAbiTargeting()) { + variantTargeting.setMultiAbiTargeting(apkTargeting.getMultiAbiTargeting()); + } + variantTargeting.setSdkVersionTargeting(sdkVersionTargeting(standaloneApk)); + + return variantTargeting.build(); + } + + private static SdkVersionTargeting sdkVersionTargeting(ModuleSplit moduleSplit) { + return SdkVersionTargeting.newBuilder() + .addValue(sdkVersionFrom(moduleSplit.getAndroidManifest().getEffectiveMinSdkVersion())) + .build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulator.java b/src/main/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulator.java index df7367e5..616538e7 100755 --- a/src/main/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulator.java +++ b/src/main/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulator.java @@ -54,6 +54,7 @@ public static GeneratedApks populateAlternativeVariantTargeting(GeneratedApks ge new SdkVersionAlternativesPopulator() .addAlternativeVariantTargeting(generatedApks.getSplitApks(), standaloneApks)) .addAll(generatedApks.getInstantApks()) + .addAll(generatedApks.getSystemApks()) .build(); return GeneratedApks.fromModuleSplits(moduleSplits); } diff --git a/src/main/java/com/android/tools/build/bundletool/targeting/TargetingComparators.java b/src/main/java/com/android/tools/build/bundletool/targeting/TargetingComparators.java index 25ed5b96..a5f3d30a 100755 --- a/src/main/java/com/android/tools/build/bundletool/targeting/TargetingComparators.java +++ b/src/main/java/com/android/tools/build/bundletool/targeting/TargetingComparators.java @@ -18,10 +18,16 @@ import static com.android.tools.build.bundletool.utils.TargetingProtoUtils.getScreenDensityDpi; import static com.google.common.collect.Comparators.emptiesFirst; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.Comparator.comparing; +import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.VariantTargeting; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Comparators; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.MoreCollectors; import com.google.common.collect.Ordering; import java.util.Comparator; @@ -43,7 +49,7 @@ public final class TargetingComparators { *

  • 32 bits < 64 bits *
  • less recent version of CPU < more recent version of CPU */ - private static final Ordering ARCHITECTURE_ORDERING = + public static final Ordering ARCHITECTURE_ORDERING = Ordering.explicit( AbiAlias.ARMEABI, AbiAlias.ARMEABI_V7A, @@ -57,13 +63,35 @@ public final class TargetingComparators { comparing(TargetingComparators::getAbi, emptiesFirst(ARCHITECTURE_ORDERING)); private static final Comparator SDK_COMPARATOR = - comparing(TargetingComparators::getMinSdk, emptiesFirst(Integer::compare)); + comparing(TargetingComparators::getMinSdk, emptiesFirst(Ordering.natural())); private static final Comparator SCREEN_DENSITY_COMPARATOR = - comparing(TargetingComparators::getScreenDensity, emptiesFirst(Integer::compare)); + comparing(TargetingComparators::getScreenDensity, emptiesFirst(Ordering.natural())); + + /** + * Comparator for sets of AbiAliases, according to ARCHITECTURE_ORDERING. + * + *

    The ABIs in a MultiAbi are not ordered, but we sort them when comparing MultiAbis, from most + * to least preferable ABI. The sorted sets are then compared lexicographically. This means that: + * + * - MultiAbis are ordered by the most preferable ABI that is different between the two (e.g. + * [x86_64] > [x86]; [x86, arm64-v8a] > [x86, armeabi-v7a]). + * + * - A set of ABIs that contains another is always larger (e.g. [x86_64, X86] > [x86_64]). + */ + public static final Comparator> MULTI_ABI_ALIAS_COMPARATOR = + comparing( + TargetingComparators::sortMultiAbi, Comparators.lexicographical(ARCHITECTURE_ORDERING)); + + @VisibleForTesting + static final Comparator MULTI_ABI_COMPARATOR = + comparing(TargetingComparators::getMultiAbi, MULTI_ABI_ALIAS_COMPARATOR); public static final Comparator VARIANT_TARGETING_COMPARATOR = - SDK_COMPARATOR.thenComparing(ABI_COMPARATOR).thenComparing(SCREEN_DENSITY_COMPARATOR); + SDK_COMPARATOR + .thenComparing(ABI_COMPARATOR) + .thenComparing(MULTI_ABI_COMPARATOR) + .thenComparing(SCREEN_DENSITY_COMPARATOR); private static Optional getMinSdk(VariantTargeting variantTargeting) { // If the variant does not have an SDK targeting, it is suitable for all SDK values. @@ -96,6 +124,25 @@ private static Optional getAbi(VariantTargeting variantTargeting) { .getAlias()); } + private static ImmutableSet getMultiAbi(VariantTargeting variantTargeting) { + if (variantTargeting.getMultiAbiTargeting().getValueList().isEmpty()) { + return ImmutableSet.of(); + } + + return variantTargeting.getMultiAbiTargeting().getValueList().stream() + // For now we only support one value in MultiAbiTargeting. + .collect(MoreCollectors.onlyElement()) + .getAbiList() + .stream() + .map(Abi::getAlias) + .collect(toImmutableSet()); + } + + /** Sort a set of ABIs from most preferable (e.g. X86_64) to least (e.g. ARMEABI_V7A). */ + private static ImmutableSortedSet sortMultiAbi(ImmutableSet abis) { + return ImmutableSortedSet.copyOf(ARCHITECTURE_ORDERING.reverse(), abis); + } + private static Optional getScreenDensity(VariantTargeting variantTargeting) { return getScreenDensityDpi(variantTargeting.getScreenDensityTargeting()); } diff --git a/src/main/java/com/android/tools/build/bundletool/utils/ResultUtils.java b/src/main/java/com/android/tools/build/bundletool/utils/ResultUtils.java index b3bc2864..4702f4ce 100755 --- a/src/main/java/com/android/tools/build/bundletool/utils/ResultUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/utils/ResultUtils.java @@ -90,7 +90,11 @@ public static ImmutableList standaloneApkVariants(ImmutableList nonStandaloneApkVariants(ImmutableList variants) { + public static ImmutableList systemApkVariants(BuildApksResult result) { + return systemApkVariants(ImmutableList.copyOf(result.getVariantList())); + } + + public static ImmutableList systemApkVariants(ImmutableList variants) { return variants.stream() .filter(variant -> !isStandaloneApkVariant(variant)) .collect(toImmutableList()); @@ -120,5 +124,11 @@ public static boolean isInstantApkVariant(Variant variant) { .allMatch(ApkDescription::hasInstantApkMetadata); } + public static boolean isSystemApkVariant(Variant variant) { + return variant.getApkSetList().stream() + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) + .allMatch(ApkDescription::hasSystemApkMetadata); + } + private ResultUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/utils/SplitsXmlInjector.java b/src/main/java/com/android/tools/build/bundletool/utils/SplitsXmlInjector.java index 9162c490..77a66d69 100755 --- a/src/main/java/com/android/tools/build/bundletool/utils/SplitsXmlInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/utils/SplitsXmlInjector.java @@ -57,6 +57,7 @@ public GeneratedApks process(GeneratedApks generatedApks) { switch (keySplit.getKey().getSplitType()) { case SPLIT: return processSplitApkVariant(keySplit.getValue()); + case SYSTEM: case STANDALONE: return keySplit.getValue().stream() .map(this::processStandaloneVariant) diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java index acbf081e..77d86008 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java @@ -30,11 +30,11 @@ import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MaxSdkLessThanMinInstantSdk; import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MinSdkGreaterThanMaxSdkException; import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MinSdkInvalidException; +import com.android.tools.build.bundletool.exceptions.manifest.ManifestVersionCodeConflictException; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.ModuleDeliveryType; import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoAttribute; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.util.Optional; @@ -42,8 +42,6 @@ /** Validates {@code AndroidManifest.xml} file of each module. */ public class AndroidManifestValidator extends SubValidator { - private static final Joiner COMMA_JOINER = Joiner.on(','); - @Override public void validateAllModules(ImmutableList modules) { validateSameVersionCode(modules); @@ -60,11 +58,7 @@ public void validateSameVersionCode(ImmutableList modules) { .collect(toImmutableList()); if (versionCodes.size() > 1) { - throw ValidationException.builder() - .withMessage( - "App Bundle modules should have the same version code but found [%s].", - COMMA_JOINER.join(versionCodes)) - .build(); + throw new ManifestVersionCodeConflictException(versionCodes.toArray(new Integer[0])); } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java index b9b26a97..38df8092 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ApexBundleValidator.java @@ -16,11 +16,18 @@ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.AbiName.ARM64_V8A; +import static com.android.tools.build.bundletool.model.AbiName.ARMEABI_V7A; +import static com.android.tools.build.bundletool.model.AbiName.X86; +import static com.android.tools.build.bundletool.model.AbiName.X86_64; +import static com.android.tools.build.bundletool.model.BundleModule.ABI_SPLITTER; import static com.android.tools.build.bundletool.model.BundleModule.APEX_DIRECTORY; +import static com.android.tools.build.bundletool.model.BundleModule.APEX_MANIFEST_PATH; import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.bundle.Files.TargetedApexImage; import com.android.tools.build.bundletool.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.AbiName; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleEntry; import com.android.tools.build.bundletool.model.ZipPath; @@ -32,7 +39,10 @@ /** Validates an APEX bundle. */ public class ApexBundleValidator extends SubValidator { - private static final ZipPath APEX_MANIFEST_PATH = ZipPath.create("root/manifest.json"); + private static final ImmutableSet> REQUIRED_SINGLETON_ABIS = + ImmutableList.of(X86_64, X86, ARMEABI_V7A, ARM64_V8A).stream() + .map(ImmutableSet::of) + .collect(toImmutableSet()); @Override public void validateAllModules(ImmutableList modules) { @@ -68,10 +78,12 @@ public void validateModule(BundleModule module) { } ImmutableSet.Builder apexImagesBuilder = ImmutableSet.builder(); + ImmutableSet.Builder apexFileNamesBuilder = ImmutableSet.builder(); for (ModuleEntry entry : module.getEntries()) { ZipPath path = entry.getPath(); if (path.startsWith(APEX_DIRECTORY)) { apexImagesBuilder.add(path.toString()); + apexFileNamesBuilder.add(path.getFileName().toString()); } else if (!path.equals(APEX_MANIFEST_PATH)) { throw ValidationException.builder() .withMessage("Unexpected file in APEX bundle: '%s'.", entry.getPath()) @@ -98,5 +110,37 @@ public void validateModule(BundleModule module) { .withMessage("Targeted APEX image files are missing: %s", missingTargetedImages) .build(); } + + ImmutableSet> allAbiNameSets = + apexFileNamesBuilder.build().stream() + .map(ApexBundleValidator::abiNamesFromFile) + .collect(toImmutableSet()); + if (allAbiNameSets.size() != apexImages.size()) { + throw ValidationException.builder() + .withMessage( + "Every APEX image file must target a unique set of architectures, " + + "but found multiple files that target the same set of architectures.") + .build(); + } + + if (!allAbiNameSets.containsAll(REQUIRED_SINGLETON_ABIS)) { + throw ValidationException.builder() + .withMessage( + "APEX bundle must contain all these singleton architectures: ." + + REQUIRED_SINGLETON_ABIS) + .build(); + } + } + + private static ImmutableSet abiNamesFromFile(String fileName) { + ImmutableList tokens = ImmutableList.copyOf(ABI_SPLITTER.splitToList(fileName)); + + // We assume that the validity of each file name was already confirmed + return tokens.stream() + // Do not include the suffix "img". + .limit(tokens.size() - 1) + .map(AbiName::fromPlatformName) + .map(Optional::get) + .collect(toImmutableSet()); } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java index a46ae327..891a5666 100755 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleFilesValidator.java @@ -27,6 +27,8 @@ import static com.android.tools.build.bundletool.model.BundleModule.RESOURCES_PROTO_PATH; import static com.android.tools.build.bundletool.model.BundleModule.ROOT_DIRECTORY; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.tools.build.bundletool.exceptions.BundleFileTypesException.FileUsesReservedNameException; import com.android.tools.build.bundletool.exceptions.BundleFileTypesException.FilesInResourceDirectoryRootException; @@ -41,6 +43,7 @@ import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import java.util.Optional; import java.util.regex.Pattern; /** Validates files inside a bundle. */ @@ -126,9 +129,7 @@ public void validateModuleFile(ZipPath file) { throw new InvalidFileExtensionInDirectoryException(APEX_DIRECTORY, ".img", file); } - if (!isMultiAbiFileName(fileName)) { - throw InvalidNativeArchitectureNameException.createForFile(file); - } + validateMultiAbiFileName(file); } else { throw new UnknownFileOrDirectoryFoundInModuleException(file); @@ -140,12 +141,25 @@ private static boolean isReservedRootApkEntry(ZipPath name) { || CLASSES_DEX_PATTERN.matcher(name.toString()).matches(); } - private static boolean isMultiAbiFileName(String fileName) { - ImmutableList tokens = ImmutableList.copyOf(ABI_SPLITTER.splitToList(fileName)); + private static void validateMultiAbiFileName(ZipPath file) { + ImmutableList tokens = + ImmutableList.copyOf(ABI_SPLITTER.splitToList(file.getFileName().toString())); int nAbis = tokens.size() - 1; + // This was validated above. checkState(tokens.get(nAbis).equals("img"), "File under 'apex/' does not have suffix 'img'"); - return tokens.stream() - .limit(nAbis) - .allMatch(token -> AbiName.fromPlatformName(token).isPresent()); + + ImmutableList> abis = + // Do not include the suffix "img". + tokens.stream().limit(nAbis).map(AbiName::fromPlatformName).collect(toImmutableList()); + if (!abis.stream().allMatch(Optional::isPresent)) { + throw InvalidNativeArchitectureNameException.createForFile(file); + } + + ImmutableSet uniqueAbis = abis.stream().map(Optional::get).collect(toImmutableSet()); + if (uniqueAbis.size() != nAbis) { + throw ValidationException.builder() + .withMessage("Repeating architectures in APEX system image file '%s'.", file) + .build(); + } } } diff --git a/src/main/java/com/android/tools/build/bundletool/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/version/BundleToolVersion.java index 33c7223d..dc24c99d 100755 --- a/src/main/java/com/android/tools/build/bundletool/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "0.7.1"; + private static final String CURRENT_VERSION = "0.7.2"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index 15aa822d..830e1a3d 100755 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -76,6 +76,8 @@ message ApkDescription { StandaloneApkMetadata standalone_apk_metadata = 4; // Set only for Instant split APKs. SplitApkMetadata instant_apk_metadata = 5; + // Set only for system APKs. + SystemApkMetadata system_apk_metadata = 6; } } @@ -92,3 +94,22 @@ message StandaloneApkMetadata { // Names of the modules fused in this standalone APK. repeated string fused_module_name = 1; } + +// Holds data specific to system APKs. +message SystemApkMetadata { + // Names of the modules fused in this system APK. + repeated string fused_module_name = 1; + enum SystemApkType { + UNSPECIFIED_VALUE = 0; + // Uncompressed APK for system image. + SYSTEM = 1; + // Stub APK for compressed APK in the system image + // (contains only android manifest). + SYSTEM_STUB = 2; + // Compressed APK for system image. + SYSTEM_COMPRESSED = 3; + } + // Indicates whether the APK is uncompressed system APK, stub APK or + // compressed system APK. + SystemApkType system_apk_type = 2; +} diff --git a/src/main/proto/errors.proto b/src/main/proto/errors.proto index cb3697f3..fe96663c 100755 --- a/src/main/proto/errors.proto +++ b/src/main/proto/errors.proto @@ -11,7 +11,7 @@ message BundleToolError { // include server paths and configuration. string exception_message = 1; - // Next id: 27 + // Next id: 28 oneof custom_error { ManifestMissingVersionCodeError manifest_missing_version_code = 2; ManifestInvalidVersionCodeError manifest_invalid_version_code = 3; @@ -26,6 +26,8 @@ message BundleToolError { ManifestMinSdkInvalidError manifest_min_sdk_invalid = 19; ManifestMinSdkGreaterThanMaxSdkError manifest_min_sdk_greater_than_max = 20; ManifestDuplicateAttributeError manifest_duplicate_attribute = 25; + ManifestModulesDifferentVersionCodes + manifest_modules_different_version_codes = 27; FileTypeInvalidFileExtensionError file_type_invalid_file_extension = 6; FileTypeInvalidFileNameInDirectoryError file_type_invalid_file_name = 7; @@ -92,6 +94,10 @@ message ManifestDuplicateAttributeError { string module_name = 2; } +message ManifestModulesDifferentVersionCodes { + repeated int32 version_codes = 1; +} + message FileTypeInvalidFileExtensionError { string bundle_directory = 1; string required_extension = 2; diff --git a/src/main/proto/targeting.proto b/src/main/proto/targeting.proto index 41977dfd..a1204ae5 100755 --- a/src/main/proto/targeting.proto +++ b/src/main/proto/targeting.proto @@ -11,6 +11,7 @@ message VariantTargeting { SdkVersionTargeting sdk_version_targeting = 1; AbiTargeting abi_targeting = 2; ScreenDensityTargeting screen_density_targeting = 3; + MultiAbiTargeting multi_abi_targeting = 4; } // Targeting on the level of individual APKs. @@ -21,6 +22,7 @@ message ApkTargeting { ScreenDensityTargeting screen_density_targeting = 4; SdkVersionTargeting sdk_version_targeting = 5; TextureCompressionFormatTargeting texture_compression_format_targeting = 6; + MultiAbiTargeting multi_abi_targeting = 7; } // Targeting on the module level. 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 8062e7e4..a3006436 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; @@ -132,6 +133,35 @@ public void buildingViaFlagsAndBuilderHasSameResult_defaults() throws Exception .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + // Remove this test when universal flag is deleted. + @Test + public void buildingViaFlagsWithUniversal_setsUniversalModeOnBuilder() throws Exception { + BuildApksCommand commandViaFlags = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH, + "--universal"), + fakeAdbServer); + + BuildApksCommand commandViaBuilder = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + // Must copy instance of the internal executor service. + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) + .setApkBuildMode(UNIVERSAL) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -160,6 +190,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalOptimizeFor() throws .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -195,6 +226,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalSigning() throws Exc .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -210,7 +242,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalUniversal() throws E "--output=" + outputFilePath, "--aapt2=" + AAPT2_PATH, // Optional values. - "--universal"), + "--mode=UNIVERSAL"), fakeAdbServer); BuildApksCommand commandViaBuilder = @@ -218,11 +250,12 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalUniversal() throws E .setBundlePath(bundlePath) .setOutputFile(outputFilePath) // Optional values. - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) // Must copy instance of the internal executor service. .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -250,6 +283,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalOverwrite() throws E .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -281,6 +315,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_deviceId() throws Exception .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -314,6 +349,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_androidSerialVariable() thro .setAapt2Command(commandViaFlags.getAapt2Command().get()) .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -363,12 +399,12 @@ public void optimizationDimensionsWithUniversal_throws() throws Exception { .setBundlePath(bundlePath) .setOutputFile(outputFilePath) .setAapt2Command(aapt2Command) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .setOptimizationDimensions(ImmutableSet.of(ABI)) .build()); assertThat(builderException) .hasMessageThat() - .contains("Cannot generate universal APK and specify optimization dimensions"); + .contains("Optimization dimension can be only set when running with 'default' mode flag."); ValidationException flagsException = assertThrows( @@ -380,11 +416,11 @@ public void optimizationDimensionsWithUniversal_throws() throws Exception { "--bundle=" + bundlePath, "--output=" + outputFilePath, "--optimize-for=abi", - "--universal"), + "--mode=UNIVERSAL"), fakeAdbServer)); assertThat(flagsException) .hasMessageThat() - .contains("Cannot generate universal APK and specify optimization dimensions"); + .contains("Optimization dimension can be only set when running with 'default' mode flag."); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksConnectedDeviceTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksConnectedDeviceTest.java index 391cafb1..c76be49c 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksConnectedDeviceTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksConnectedDeviceTest.java @@ -15,6 +15,7 @@ */ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractTocFromApkSetFile; import static com.android.tools.build.bundletool.testing.AppBundleFactory.createLdpiHdpiAppBundle; import static com.android.tools.build.bundletool.testing.AppBundleFactory.createMaxSdkBundle; @@ -124,6 +125,7 @@ public void connectedDevice_flagsEquivalent_androidHome() { // Must copy instance of the internal executor service. .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -157,6 +159,7 @@ public void connectedDevice_flagsEquivalent_explicitAdbPath() throws Exception { // Must copy instance of the internal executor service. .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -189,6 +192,7 @@ public void connectedDevice_flagsEquivalent_deviceId() { // Must copy instance of the internal executor service. .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -207,7 +211,7 @@ public void connectedDevice_universalApk_throws() throws Exception { .setBundlePath(bundlePath) .setOutputFile(outputFilePath) .setGenerateOnlyForConnectedDevice(true) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .setAdbPath(sdkDirPath.resolve("platform-tools").resolve("adb")) .setAdbServer(fakeAdbServer); @@ -215,8 +219,7 @@ public void connectedDevice_universalApk_throws() throws Exception { assertThat(exception) .hasMessageThat() .contains( - "Cannot generate universal APK and optimize for the connected " - + "device at the same time."); + "Optimizing for connected device only possible when running with 'default' mode flag."); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java index 4860046f..81890299 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java @@ -15,6 +15,7 @@ */ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractTocFromApkSetFile; import static com.android.tools.build.bundletool.testing.AppBundleFactory.createInstantBundle; import static com.android.tools.build.bundletool.testing.AppBundleFactory.createLdpiHdpiAppBundle; @@ -107,6 +108,7 @@ public void deviceSpec_flagsEquivalent() throws Exception { // Must copy instance of the internal executor service. .setExecutorServiceInternal(commandViaFlags.getExecutorService()) .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -127,13 +129,13 @@ public void deviceSpec_universalApk_throws() throws Exception { .setBundlePath(bundlePath) .setOutputFile(outputFilePath) .setDeviceSpecPath(deviceSpecPath) - .setGenerateOnlyUniversalApk(true); + .setApkBuildMode(UNIVERSAL); Throwable exception = assertThrows(ValidationException.class, () -> command.build()); assertThat(exception) .hasMessageThat() .contains( - "Cannot generate universal APK and optimize for the device spec at the same time."); + "Optimizing for device spec only possible when running with 'default' mode flag."); } @Test 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 ed89849a..51f1846c 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -16,14 +16,22 @@ package com.android.tools.build.bundletool.commands; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ATC; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ETC1_RGB8; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM_COMPRESSED; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractFromApkSetFile; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractTocFromApkSetFile; import static com.android.tools.build.bundletool.testing.ApkSetUtils.parseTocFromFile; +import static com.android.tools.build.bundletool.testing.FileUtils.uncompressGzipFile; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForFeature; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFusingAttribute; @@ -46,6 +54,9 @@ import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.resourceTableWithTestLabel; import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImages; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargetingFromAllTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assets; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; @@ -54,27 +65,32 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.nativeLibraries; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedApexImage; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedNativeDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.toAbi; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMultiAbiTargetingFromAllTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.filesUnderPath; import static com.android.tools.build.bundletool.testing.truth.zip.TruthZip.assertThat; import static com.android.tools.build.bundletool.utils.ResultUtils.instantApkVariants; import static com.android.tools.build.bundletool.utils.ResultUtils.splitApkVariants; import static com.android.tools.build.bundletool.utils.ResultUtils.standaloneApkVariants; +import static com.android.tools.build.bundletool.utils.ResultUtils.systemApkVariants; import static com.android.tools.build.bundletool.utils.Versions.ANDROID_M_API_VERSION; import static com.android.tools.build.bundletool.utils.Versions.ANDROID_P_API_VERSION; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.aapt.ConfigurationOuterClass.Configuration; @@ -84,13 +100,19 @@ import com.android.bundle.Commands.ApkSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.ModuleMetadata; +import com.android.bundle.Commands.SystemApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata.SystemApkType; import com.android.bundle.Commands.Variant; +import com.android.bundle.Config.SplitDimension.Value; +import com.android.bundle.Files.ApexImages; import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.TestData; +import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.device.AdbServer; import com.android.tools.build.bundletool.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.exceptions.ValidationException; @@ -110,6 +132,7 @@ import com.android.tools.build.bundletool.testing.CertificateFactory; import com.android.tools.build.bundletool.testing.FakeAdbServer; import com.android.tools.build.bundletool.testing.FileUtils; +import com.android.tools.build.bundletool.testing.truth.zip.TruthZip; import com.android.tools.build.bundletool.utils.files.FilePreconditions; import com.android.tools.build.bundletool.utils.flags.FlagParser; import com.android.tools.build.bundletool.utils.flags.ParsedFlags; @@ -118,14 +141,15 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; @@ -146,11 +170,14 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(Theories.class) public class BuildApksManagerTest { private static final int ANDROID_L_API_VERSION = 21; @@ -174,7 +201,6 @@ public class BuildApksManagerTest { private Path bundlePath; private Path outputDir; private Path outputFilePath; - private Path keystorePath; private final AdbServer fakeAdbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, /* devices= */ ImmutableList.of()); @@ -197,7 +223,7 @@ public void setUp() throws Exception { outputFilePath = outputDir.resolve("app.apks"); // KeyStore. - keystorePath = tmpDir.resolve("keystore.jks"); + Path keystorePath = tmpDir.resolve("keystore.jks"); KeyStore keystore = KeyStore.getInstance("JKS"); keystore.load(/* stream= */ null, KEYSTORE_PASSWORD.toCharArray()); keystore.setKeyEntry( @@ -320,6 +346,109 @@ public void selectsRightModules() throws Exception { .containsExactly("assets/base.txt", "assets/fused.txt"); } + @DataPoints("systemApkBuildModes") + public static final ImmutableSet SYSTEM_APK_BUILD_MODE = + ImmutableSet.of(SYSTEM, SYSTEM_COMPRESSED); + + @Test + @Theory + public void selectsRightModules_systemApks( + @FromDataPoints("systemApkBuildModes") ApkBuildMode systemApkBuildMode) throws Exception { + Path bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/base.txt") + .setManifest(androidManifest("com.app")) + .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .addModule( + "fused", + module -> + module + .addFile("assets/fused.txt") + .setManifest( + androidManifestForFeature( + "com.app", + withFusingAttribute(true), + 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)))) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + Path apkSetFilePath = + execute( + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .setApkBuildMode(systemApkBuildMode) + .build()); + + ZipFile apkSetFile = new ZipFile(apkSetFilePath.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)); + + File systemApkFile; + if (systemApkBuildMode.equals(SYSTEM)) { + assertThat(systemApks) + .containsExactly( + ApkDescription.newBuilder() + .setPath("system/system.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .setSystemApkType(SystemApkType.SYSTEM) + .addAllFusedModuleName(ImmutableList.of("base", "fused"))) + .build()); + systemApkFile = extractFromApkSetFile(apkSetFile, "system/system.apk", outputDir); + } else { + assertThat(systemApks) + .containsExactly( + ApkDescription.newBuilder() + .setPath("system/system.apk") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .setSystemApkType(SystemApkType.SYSTEM_STUB) + .addAllFusedModuleName(ImmutableList.of("base", "fused"))) + .build(), + ApkDescription.newBuilder() + .setPath("system/system.apk.gz") + .setTargeting(ApkTargeting.getDefaultInstance()) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .setSystemApkType(SystemApkType.SYSTEM_COMPRESSED) + .addAllFusedModuleName(ImmutableList.of("base", "fused"))) + .build()); + // Uncompress the compressed APK + systemApkFile = + uncompressGzipFile( + extractFromApkSetFile(apkSetFile, "system/system.apk.gz", outputDir).toPath(), + outputDir.resolve("output.apk")) + .toFile(); + } + // Validate that the system APK contains appropriate files. + ZipFile systemApkZip = new ZipFile(systemApkFile); + assertThat(filesUnderPath(systemApkZip, ZipPath.create("assets"))) + .containsExactly("assets/base.txt", "assets/fused.txt"); + } + @Test public void bundleWithDirectoryZipEntries_throws() throws Exception { Path tmpBundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); @@ -542,7 +671,7 @@ public void buildApksCommand_universal_selectsRightModulesForMerging() throws Ex BuildApksCommand.builder() .setBundlePath(bundlePath) .setOutputFile(outputFilePath) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .setAapt2Command(aapt2Command) .build(); @@ -602,7 +731,7 @@ public void buildApksCommand_universal_generatesSingleApkWithNoOptimizations() t BuildApksCommand.builder() .setBundlePath(bundlePath) .setOutputFile(outputFilePath) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .setAapt2Command(aapt2Command) .build(); @@ -634,6 +763,177 @@ public void buildApksCommand_universal_generatesSingleApkWithNoOptimizations() t } } + @Test + public void buildApksCommand_compressedSystem_generatesSingleApkWithEmptyOptimizations() + throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + // Add some native libraries. + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + // Add some density-specific resources. + .addFile("res/drawable-ldpi/image.jpg") + .addFile("res/drawable-mdpi/image.jpg") + .setResourceTable( + createResourceTable( + "image", + fileReference("res/drawable-ldpi/image.jpg", LDPI), + fileReference("res/drawable-mdpi/image.jpg", MDPI))) + .setManifest(androidManifest("com.test.app"))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension(Value.ABI, /* negate= */ true) + .addSplitDimension(Value.SCREEN_DENSITY, /* negate= */ true) + .build()) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setApkBuildMode(SYSTEM_COMPRESSED) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + // Should not shard by any dimension and generate single APK with default targeting. + assertThat(result.getVariantList()).hasSize(1); + assertThat(splitApkVariants(result)).isEmpty(); + assertThat(standaloneApkVariants(result)).isEmpty(); + assertThat(systemApkVariants(result)).hasSize(1); + + Variant systemVariant = systemApkVariants(result).get(0); + assertThat(systemVariant.getTargeting()).isEqualTo(UNRESTRICTED_VARIANT_TARGETING); + + assertThat(apkDescriptions(systemVariant)).hasSize(2); + + ImmutableMap apkTypeApkMap = + Maps.uniqueIndex( + apkDescriptions(systemVariant), + apkDescription -> apkDescription.getSystemApkMetadata().getSystemApkType()); + assertThat(apkTypeApkMap.keySet()) + .containsExactly(SystemApkType.SYSTEM_STUB, SystemApkType.SYSTEM_COMPRESSED); + + ApkDescription stubSystemApk = apkTypeApkMap.get(SystemApkType.SYSTEM_STUB); + + assertThat(stubSystemApk.getTargeting()).isEqualToDefaultInstance(); + File systemApkFile = extractFromApkSetFile(apkSetFile, stubSystemApk.getPath(), outputDir); + try (ZipFile systemApkZipFile = new ZipFile(systemApkFile)) { + assertThat(systemApkZipFile) + .containsExactlyEntries("AndroidManifest.xml", "META-INF/MANIFEST.MF"); + } + + ApkDescription compressedSystemApk = apkTypeApkMap.get(SystemApkType.SYSTEM_COMPRESSED); + Path compressedSystemApkFile = + uncompressGzipFile( + extractFromApkSetFile(apkSetFile, compressedSystemApk.getPath(), outputDir).toPath(), + outputDir.resolve("output.apk")); + try (ZipFile compressedSystemApkZipFile = new ZipFile(compressedSystemApkFile.toFile())) { + // "res/xml/splits0.xml" is created by bundletool with list of generated splits. + assertThat(compressedSystemApkZipFile) + .containsExactlyEntries( + "AndroidManifest.xml", + "META-INF/MANIFEST.MF", + "lib/x86/libsome.so", + "lib/x86_64/libsome.so", + "res/drawable-ldpi/image.jpg", + "res/drawable-mdpi/image.jpg", + "res/xml/splits0.xml", + "resources.arsc"); + } + } + + @Test + public void buildApksCommand_system_generatesSingleApkWithEmptyOptimizations() throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + // Add some native libraries. + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + // Add some density-specific resources. + .addFile("res/drawable-ldpi/image.jpg") + .addFile("res/drawable-mdpi/image.jpg") + .setResourceTable( + createResourceTable( + "image", + fileReference("res/drawable-ldpi/image.jpg", LDPI), + fileReference("res/drawable-mdpi/image.jpg", MDPI))) + .setManifest(androidManifest("com.test.app"))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension(Value.ABI, /* negate= */ true) + .addSplitDimension(Value.SCREEN_DENSITY, /* negate= */ true) + .build()) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setApkBuildMode(SYSTEM) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + // Should not shard by any dimension and generate single APK with default targeting. + assertThat(result.getVariantList()).hasSize(1); + assertThat(splitApkVariants(result)).isEmpty(); + assertThat(standaloneApkVariants(result)).isEmpty(); + assertThat(systemApkVariants(result)).hasSize(1); + + Variant systemVariant = systemApkVariants(result).get(0); + assertThat(systemVariant.getTargeting()).isEqualTo(UNRESTRICTED_VARIANT_TARGETING); + + assertThat(apkDescriptions(systemVariant)).hasSize(1); + ApkDescription systemApk = apkDescriptions(systemVariant).get(0); + assertThat(systemApk.getTargeting()).isEqualToDefaultInstance(); + + File systemApkFile = extractFromApkSetFile(apkSetFile, systemApk.getPath(), outputDir); + try (ZipFile systemApkZipFile = new ZipFile(systemApkFile)) { + // "res/xml/splits0.xml" is created by bundletool with list of generated splits. + TruthZip.assertThat(systemApkZipFile) + .containsExactlyEntries( + "AndroidManifest.xml", + "META-INF/MANIFEST.MF", + "lib/x86/libsome.so", + "lib/x86_64/libsome.so", + "res/drawable-ldpi/image.jpg", + "res/drawable-mdpi/image.jpg", + "res/xml/splits0.xml", + "resources.arsc"); + } + } + @Test public void buildApksCommand_universalApk_variantUsesMinSdkFromManifest() throws Exception { AppBundle appBundle = @@ -652,7 +952,7 @@ public void buildApksCommand_universalApk_variantUsesMinSdkFromManifest() throws BuildApksCommand.builder() .setBundlePath(bundlePath) .setOutputFile(outputFilePath) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .setAapt2Command(aapt2Command) .build(); @@ -832,9 +1132,8 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception Maps.uniqueIndex( standaloneApkVariants(result), variant -> { - ApkDescription apkDescription = Iterables.getOnlyElement(apkDescriptions(variant)); - return Iterables.getOnlyElement( - apkDescription.getTargeting().getAbiTargeting().getValueList()); + ApkDescription apkDescription = getOnlyElement(apkDescriptions(variant)); + return getOnlyElement(apkDescription.getTargeting().getAbiTargeting().getValueList()); }); assertThat(standaloneVariantsByAbi.keySet()) .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.X86_64), toAbi(AbiAlias.MIPS)); @@ -876,6 +1175,102 @@ public void buildApksCommand_standalone_oneModuleManyVariants() throws Exception } } + @Test + @Theory + public void buildApksCommand_system_oneModuleManyVariants( + @FromDataPoints("systemApkBuildModes") ApkBuildMode systemApkBuildMode) throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .addFile("lib/mips/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)), + targetedNativeDirectory( + "lib/mips", nativeDirectoryTargeting(AbiAlias.MIPS)))) + .setManifest(androidManifest("com.test.app"))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension(Value.ABI) + .addSplitDimension(Value.SCREEN_DENSITY, /* negate= */ true) + .build()) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setApkBuildMode(systemApkBuildMode) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + ImmutableMap systemVariantsByAbi; + if (systemApkBuildMode.equals(SYSTEM)) { + systemVariantsByAbi = + Maps.uniqueIndex( + systemApkVariants(result), + variant -> { + ApkDescription apkDescription = getOnlyElement(apkDescriptions(variant)); + return getOnlyElement( + apkDescription.getTargeting().getAbiTargeting().getValueList()); + }); + + } else { + systemVariantsByAbi = + Maps.uniqueIndex( + systemApkVariants(result), + variant -> { + assertThat(apkDescriptions(variant)).hasSize(2); + ApkDescription apkDescription = apkDescriptions(variant).get(0); + return getOnlyElement( + apkDescription.getTargeting().getAbiTargeting().getValueList()); + }); + } + + assertThat(systemVariantsByAbi.keySet()) + .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.X86_64), toAbi(AbiAlias.MIPS)); + assertThat(systemVariantsByAbi.get(toAbi(AbiAlias.X86)).getTargeting()) + .ignoringRepeatedFieldOrder() + .isEqualTo( + mergeVariantTargeting( + variantAbiTargeting(AbiAlias.X86, ImmutableSet.of(AbiAlias.X86_64, AbiAlias.MIPS)), + variantSdkTargeting(LOWEST_SDK_VERSION))); + assertThat(systemVariantsByAbi.get(toAbi(AbiAlias.X86_64)).getTargeting()) + .ignoringRepeatedFieldOrder() + .isEqualTo( + mergeVariantTargeting( + variantAbiTargeting(AbiAlias.X86_64, ImmutableSet.of(AbiAlias.X86, AbiAlias.MIPS)), + variantSdkTargeting(LOWEST_SDK_VERSION))); + assertThat(systemVariantsByAbi.get(toAbi(AbiAlias.MIPS)).getTargeting()) + .ignoringRepeatedFieldOrder() + .isEqualTo( + mergeVariantTargeting( + variantAbiTargeting(AbiAlias.MIPS, ImmutableSet.of(AbiAlias.X86, AbiAlias.X86_64)), + variantSdkTargeting(LOWEST_SDK_VERSION))); + for (Variant variant : systemVariantsByAbi.values()) { + assertThat(variant.getApkSetList()).hasSize(1); + ApkSet apkSet = variant.getApkSet(0); + apkSet + .getApkDescriptionList() + .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); + } + } + @Test public void buildApksCommand_standalone_mixedTargeting() throws Exception { bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); @@ -958,8 +1353,7 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { ImmutableMap standaloneApksByAbi = Maps.uniqueIndex( apkDescriptions(standaloneApkVariants(result)), - apkDesc -> - Iterables.getOnlyElement(apkDesc.getTargeting().getAbiTargeting().getValueList())); + apkDesc -> getOnlyElement(apkDesc.getTargeting().getAbiTargeting().getValueList())); assertThat(standaloneApksByAbi.keySet()) .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.X86_64)); @@ -1267,6 +1661,160 @@ public void buildApksCommand_inconsistentAbis_discarded() throws Exception { } } + @Test + public void buildApksCommand_apexBundle() throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + ApexImages apexConfig = + apexImages( + targetedApexImage("apex/x86_64.x86.img", apexImageTargeting("x86_64", "x86")), + targetedApexImage( + "apex/x86_64.armeabi-v7a.img", apexImageTargeting("x86_64", "armeabi-v7a")), + targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64")), + targetedApexImage("apex/x86.armeabi-v7a.img", apexImageTargeting("x86", "armeabi-v7a")), + targetedApexImage("apex/x86.img", apexImageTargeting("x86")), + targetedApexImage("apex/arm64-v8a.img", apexImageTargeting("arm64-v8a")), + targetedApexImage("apex/armeabi-v7a.img", apexImageTargeting("armeabi-v7a"))); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.x86.img") + .addFile("apex/x86_64.armeabi-v7a.img") + .addFile("apex/x86_64.img") + .addFile("apex/x86.armeabi-v7a.img") + .addFile("apex/x86.img") + .addFile("apex/arm64-v8a.img") + .addFile("apex/armeabi-v7a.img") + .setApexConfig(apexConfig) + .setManifest(androidManifest("com.test.app"))) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + Path apkSetFilePath = + execute( + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .build()); + + ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); + ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64Set = ImmutableSet.of(X86_64); + ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86Set = ImmutableSet.of(X86); + ImmutableSet arm8Set = ImmutableSet.of(ARM64_V8A); + ImmutableSet arm7Set = ImmutableSet.of(ARMEABI_V7A); + + ImmutableSet> allTargeting = + ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, arm8Set, arm7Set); + ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); + ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); + ApkTargeting x64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); + ApkTargeting x86ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x86ArmSet, allTargeting); + ApkTargeting x86Targeting = apkMultiAbiTargetingFromAllTargeting(x86Set, allTargeting); + ApkTargeting arm8Targeting = apkMultiAbiTargetingFromAllTargeting(arm8Set, allTargeting); + ApkTargeting arm7Targeting = apkMultiAbiTargetingFromAllTargeting(arm7Set, allTargeting); + + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + ImmutableMap standaloneVariantsByAbi = + extractStandaloneVariantsByTargeting(result); + assertThat(standaloneVariantsByAbi.keySet()) + .containsExactly( + x64X86Targeting, + x64ArmTargeting, + x64Targeting, + x86ArmTargeting, + x86Targeting, + arm8Targeting, + arm7Targeting); + + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(x64X86Targeting), + variantMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(x64ArmTargeting), + variantMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(x64Targeting), + variantMultiAbiTargetingFromAllTargeting(x64Set, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(x86ArmTargeting), + variantMultiAbiTargetingFromAllTargeting(x86ArmSet, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(x86Targeting), + variantMultiAbiTargetingFromAllTargeting(x86Set, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(arm8Targeting), + variantMultiAbiTargetingFromAllTargeting(arm8Set, allTargeting)); + checkVariantMultiAbiTargeting( + standaloneVariantsByAbi.get(arm7Targeting), + variantMultiAbiTargetingFromAllTargeting(arm7Set, allTargeting)); + for (Variant variant : standaloneVariantsByAbi.values()) { + ApkSet apkSet = getOnlyElement(variant.getApkSetList()); + ApkDescription apkDescription = getOnlyElement(apkSet.getApkDescriptionList()); + assertThat(apkSetFile).hasFile(apkDescription.getPath()); + } + } + + @Test + public void buildApksCommand_apexBundle_hasRightSuffix() throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + ApexImages apexConfig = + apexImages( + targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64")), + targetedApexImage("apex/x86.img", apexImageTargeting("x86")), + targetedApexImage("apex/arm64-v8a.img", apexImageTargeting("arm64-v8a")), + targetedApexImage("apex/armeabi-v7a.img", apexImageTargeting("armeabi-v7a")), + targetedApexImage( + "apex/arm64-v8a.armeabi-v7a.img", apexImageTargeting("arm64-v8a", "armeabi-v7a"))); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.img") + .addFile("apex/x86.img") + .addFile("apex/arm64-v8a.img") + .addFile("apex/armeabi-v7a.img") + .addFile("apex/arm64-v8a.armeabi-v7a.img") + .setApexConfig(apexConfig) + .setManifest(androidManifest("com.test.app"))) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + Path apkSetFilePath = + execute( + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .build()); + + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + ImmutableList variants = standaloneApkVariants(result); + + ImmutableSet apkPaths = + variants.stream() + .map( + variant -> + getOnlyElement(getOnlyElement(variant.getApkSetList()).getApkDescriptionList()) + .getPath()) + .collect(toImmutableSet()); + assertThat(apkPaths) + .containsExactly( + "standalones/standalone-x86_64.apk", + "standalones/standalone-x86.apk", + "standalones/standalone-arm64_v8a.apk", + "standalones/standalone-armeabi_v7a.apk", + "standalones/standalone-arm64_v8a.armeabi_v7a.apk"); + } + /** * This test executes the command with a reasonably complex bundle large number of times in the * hope to catch concurrency issues. @@ -1996,6 +2544,120 @@ public void buildApksCommand_instantApksAndSplitsGenerated() throws Exception { assertThat(apkSetFile).hasFile(apkSet.getApkDescription(0).getPath()); } + @Test + public void renderscript32Bit_warningMessageDisplayed() throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("assets/script.bc") + .setManifest(androidManifest("com.test.app"))) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .setOutputPrintStream(new PrintStream(output)) + .build(); + + execute(command); + assertThat(new String(output.toByteArray(), UTF_8)) + .contains("WARNING: App Bundle contains 32-bit RenderScript bitcode file (.bc)"); + } + + @Test + public void renderscript32Bit_64BitStandaloneAndSplitApksFilteredOut() throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("assets/script.bc") + .addFile("lib/armeabi-v7a/libfoo.so") + .addFile("lib/arm64-v8a/libfoo.so") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(14))) + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/armeabi-v7a", nativeDirectoryTargeting(ARMEABI_V7A)), + targetedNativeDirectory( + "lib/arm64-v8a", nativeDirectoryTargeting(ARM64_V8A))))) + .setBundleConfig( + BundleConfigBuilder.create().setUncompressNativeLibraries(false).build()) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = new ZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(standaloneApkVariants(result)).hasSize(1); + assertThat(standaloneApkVariants(result).get(0).getTargeting().getAbiTargeting()) + .isEqualTo(abiTargeting(ARMEABI_V7A)); + assertThat(splitApkVariants(result)).hasSize(1); + ImmutableSet abiTargetings = + splitApkVariants(result).get(0).getApkSetList().stream() + .map(ApkSet::getApkDescriptionList) + .flatMap(list -> list.stream().map(ApkDescription::getTargeting)) + .map(ApkTargeting::getAbiTargeting) + .collect(toImmutableSet()); + assertThat(abiTargetings) + .containsExactly(AbiTargeting.getDefaultInstance(), abiTargeting(ARMEABI_V7A)); + } + + @Test + public void renderscript32Bit_64BitLibsOnly_throws() throws Exception { + bundlePath = FileUtils.getRandomFilePath(tmp, "bundle-", ".aab"); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile("assets/script.bc") + .addFile("lib/arm64-v8a/libfoo.so") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(14))) + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory( + "lib/arm64-v8a", nativeDirectoryTargeting(ARM64_V8A))))) + .build(); + bundleSerializer.writeToDisk(appBundle, bundlePath); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .build(); + + CommandExecutionException exception = + assertThrows(CommandExecutionException.class, () -> command.execute()); + assertThat(exception) + .hasMessageThat() + .contains( + "Generation of 64-bit native libraries is " + + "disabled, but App Bundle contains only 64-bit native libraries"); + } + private static ImmutableList apkDescriptions(List variants) { return variants.stream() .flatMap(variant -> apkDescriptions(variant).stream()) @@ -2093,4 +2755,18 @@ private static int makeResourceIdentifier(int pkgId, int typeId, int entryId) { .build() .getFullResourceId(); } + + private ImmutableMap extractStandaloneVariantsByTargeting( + BuildApksResult result) { + return Maps.uniqueIndex( + standaloneApkVariants(result), + variant -> getOnlyElement(apkDescriptions(variant)).getTargeting()); + } + + private void checkVariantMultiAbiTargeting(Variant variant, VariantTargeting targeting) + throws Exception { + assertThat(variant.getTargeting()) + .ignoringRepeatedFieldOrder() + .isEqualTo(mergeVariantTargeting(targeting, variantSdkTargeting(LOWEST_SDK_VERSION))); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java index 07253cd5..5a9f85aa 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildBundleCommandTest.java @@ -24,7 +24,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withUsesSplit; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.createResourceTable; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.fileReference; -import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImageSingleAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.graphicsApiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeAssetsTargeting; @@ -239,13 +239,14 @@ public void validApexModule() throws Exception { .addImage( TargetedApexImage.newBuilder() .setPath("apex/x86.img") - .setTargeting(apexImageSingleAbiTargeting(X86))) + .setTargeting(apexImageTargeting("x86"))) .build(); Path module = new ZipBuilder() .addFileWithContent(ZipPath.create("apex/x86.img"), "apex".getBytes(UTF_8)) .addFileWithProtoContent(ZipPath.create("manifest/AndroidManifest.xml"), manifest) - .addFileWithContent(ZipPath.create("root/manifest.json"), "manifest".getBytes(UTF_8)) + .addFileWithContent( + ZipPath.create("root/apex_manifest.json"), "manifest".getBytes(UTF_8)) .writeTo(tmpDir.resolve("base.zip")); BuildBundleCommand.builder() @@ -259,7 +260,9 @@ public void validApexModule() throws Exception { .hasFile("base/manifest/AndroidManifest.xml") .withContent(manifest.toByteArray()); assertThat(bundle).hasFile("base/apex/x86.img").withContent("apex".getBytes(UTF_8)); - assertThat(bundle).hasFile("base/root/manifest.json").withContent("manifest".getBytes(UTF_8)); + assertThat(bundle) + .hasFile("base/root/apex_manifest.json") + .withContent("manifest".getBytes(UTF_8)); assertThat(bundle).hasFile("base/apex.pb").withContent(apexConfig.toByteArray()); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java index f94a0048..2b1ca40f 100755 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -16,6 +16,9 @@ package com.android.tools.build.bundletool.commands; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksArchiveFile; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksDirectory; @@ -26,6 +29,7 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createStandaloneApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariantForSingleSplitApk; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.multiAbiTargetingStandaloneVariant; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.createDeviceSpecFile; import static com.android.tools.build.bundletool.testing.DeviceFactory.density; @@ -38,6 +42,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeModuleTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleFeatureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleMinSdkVersionTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.multiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; @@ -50,6 +55,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.MultiAbiTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.VariantTargeting; @@ -592,6 +598,77 @@ public void oneModule_Ldevice_matchesLSplit( } } + @Test + public void apexModule_noMatch() throws Exception { + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addVariant( + multiAbiTargetingStandaloneVariant( + multiAbiTargeting(X86_64), ZipPath.create("standalones/standalone-x86_64.apk"))) + .build(); + + Path apksPath = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + ExtractApksCommand.Builder extractedApksCommand = + ExtractApksCommand.builder().setApksArchivePath(apksPath).setDeviceSpec(abis("x86")); + + Throwable exception = + assertThrows(CommandExecutionException.class, () -> extractedApksCommand.build().execute()); + assertThat(exception) + .hasMessageThat() + .contains( + "No set of ABI architectures that the app supports is contained in the ABI " + + "architecture set of the device."); + } + + @Test + @Theory + public void apexModule_getsBestPossibleApk( + @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + ZipPath x64Apk = ZipPath.create("standalones/standalone-x86_64.apk"); + ZipPath x64X86Apk = ZipPath.create("standalones/standalone-x86_64.x86.apk"); + ZipPath x64ArmApk = ZipPath.create("standalones/standalone-x86_64.arm64_v8a.apk"); + + MultiAbiTargeting x64Targeting = + multiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(X86_64)), + ImmutableSet.of(ImmutableSet.of(X86_64, X86), ImmutableSet.of(X86_64, ARM64_V8A))); + MultiAbiTargeting x64X86Targeting = + multiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(X86_64, X86)), + ImmutableSet.of(ImmutableSet.of(X86_64), ImmutableSet.of(X86_64, ARM64_V8A))); + MultiAbiTargeting x64ArmTargeting = + multiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(X86_64, ARM64_V8A)), + ImmutableSet.of(ImmutableSet.of(X86_64), ImmutableSet.of(X86_64, X86))); + + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addVariant(multiAbiTargetingStandaloneVariant(x64Targeting, x64Apk)) + .addVariant(multiAbiTargetingStandaloneVariant(x64X86Targeting, x64X86Apk)) + .addVariant(multiAbiTargetingStandaloneVariant(x64ArmTargeting, x64ArmApk)) + .build(); + + Path apksPath = createApks(buildApksResult, apksInDirectory); + ExtractApksCommand.Builder extractedApksCommand = + ExtractApksCommand.builder() + .setApksArchivePath(apksPath) + .setDeviceSpec(abis("x86_64", "x86")); + if (!apksInDirectory) { + extractedApksCommand.setOutputDirectory(tmpDir); + } + + ImmutableList matchedApks = extractedApksCommand.build().execute(); + + if (apksInDirectory) { + assertThat(matchedApks).containsExactly(inTempDirectory(x64X86Apk.toString())); + } else { + assertThat(matchedApks).containsExactly(inOutputDirectory(x64X86Apk.getFileName())); + } + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + @Test public void oneModule_Kdevice_noMatchingSdkVariant_throws() throws Exception { ZipPath apkL = ZipPath.create("splits/apkL.apk"); diff --git a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java index 7f92177f..e0005a26 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java @@ -31,6 +31,7 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createConditionalApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.instantApkDescription; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.multiAbiTargetingStandaloneVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.splitApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.standaloneVariant; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; @@ -53,6 +54,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleFeatureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleMinSdkVersionTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.multiAbiTargeting; 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; @@ -66,6 +68,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.MultiAbiTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.exceptions.CommandExecutionException; @@ -457,6 +460,67 @@ public void variantNotMatchingAtAbi_SdkAbiDensity() { .isEmpty(); } + // APEX variants tests. + + @Test + public void apexVariantMatch_noMatch_throws() { + ZipPath x86Apk = ZipPath.create("standalone-x86.apk"); + ZipPath x64X86Apk = ZipPath.create("standalone-x86_64.x86.apk"); + + ImmutableSet> x86Set = ImmutableSet.of(ImmutableSet.of(X86)); + ImmutableSet> x64X86Set = ImmutableSet.of(ImmutableSet.of(X86_64, X86)); + + MultiAbiTargeting x86Targeting = multiAbiTargeting(x86Set, x64X86Set); + MultiAbiTargeting x64X86Targeting = multiAbiTargeting(x64X86Set, x86Set); + + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addVariant(multiAbiTargetingStandaloneVariant(x86Targeting, x86Apk)) + .addVariant(multiAbiTargetingStandaloneVariant(x64X86Targeting, x64X86Apk)) + .build(); + + CommandExecutionException e = + assertThrows( + CommandExecutionException.class, + () -> new ApkMatcher(abis("x86_64", "armeabi-v7a")).getMatchingApks(buildApksResult)); + assertThat(e) + .hasMessageThat() + .contains( + "No set of ABI architectures that the app supports is contained in the ABI " + + "architecture set of the device"); + } + + @Test + public void apexVariantMatch_matchesRightVariant() { + ZipPath x86Apk = ZipPath.create("standalone-x86.apk"); + ZipPath x64X86Apk = ZipPath.create("standalone-x86_64.x86.apk"); + + ImmutableSet> x86Set = ImmutableSet.of(ImmutableSet.of(X86)); + ImmutableSet> x64X86Set = ImmutableSet.of(ImmutableSet.of(X86_64, X86)); + + MultiAbiTargeting x86Targeting = multiAbiTargeting(x86Set, x64X86Set); + MultiAbiTargeting x64X86Targeting = multiAbiTargeting(x64X86Set, x86Set); + + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addVariant(multiAbiTargetingStandaloneVariant(x86Targeting, x86Apk)) + .addVariant(multiAbiTargetingStandaloneVariant(x64X86Targeting, x64X86Apk)) + .build(); + + assertThat(new ApkMatcher(abis("x86")).getMatchingApks(buildApksResult)) + .containsExactly(x86Apk); + assertThat(new ApkMatcher(abis("x86_64", "x86")).getMatchingApks(buildApksResult)) + .containsExactly(x64X86Apk); + assertThat( + new ApkMatcher(abis("x86_64", "x86", "armeabi-v7a")).getMatchingApks(buildApksResult)) + .containsExactly(x64X86Apk); + // Other device specs don't affect the matching variant. + assertThat( + new ApkMatcher(deviceWith(26, ImmutableList.of("x86"), HDPI)) + .getMatchingApks(buildApksResult)) + .containsExactly(x86Apk); + } + private static DeviceSpec deviceWith( int sdkVersion, ImmutableList abis, DensityAlias densityAlias) { return mergeSpecs( diff --git a/src/test/java/com/android/tools/build/bundletool/device/MultiAbiMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/MultiAbiMatcherTest.java new file mode 100755 index 00000000..c0198ca3 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/MultiAbiMatcherTest.java @@ -0,0 +1,171 @@ +/* + * 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.device; + +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; +import static com.android.tools.build.bundletool.testing.DeviceFactory.lDeviceWithAbis; +import static com.android.tools.build.bundletool.testing.TargetingUtils.multiAbiTargeting; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.tools.build.bundletool.exceptions.CommandExecutionException; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class MultiAbiMatcherTest { + + @Test + public void checkDeviceCompatibleInternal_compatibleDevice() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86", "armeabi-v7a")); + + // The set (x86) is valid. + matcher.checkDeviceCompatible(multiAbiTargeting(X86)); + matcher.checkDeviceCompatible(multiAbiTargeting(setOf(setOf(ARM64_V8A), setOf(X86)))); + matcher.checkDeviceCompatible(multiAbiTargeting(ARM64_V8A, setOf(X86))); + // The set (x86, armeabi-v7a) is valid. + matcher.checkDeviceCompatible(multiAbiTargeting(setOf(setOf(X86, ARMEABI_V7A)))); + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(ARM64_V8A), setOf(X86, ARMEABI_V7A)))); + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(ARM64_V8A)), setOf(setOf(X86, ARMEABI_V7A)))); + } + + @Test + public void checkDeviceCompatibleInternal_compatibleDeviceInWrongOrder() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("armeabi-v7a", "x86")); + + matcher.checkDeviceCompatible(multiAbiTargeting(setOf(setOf(X86, ARMEABI_V7A)))); + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(ARM64_V8A), setOf(X86, ARMEABI_V7A)))); + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(ARM64_V8A)), setOf(setOf(X86, ARMEABI_V7A)))); + } + + @Test + public void checkDeviceCompatibleInternal_incompatibleDevice() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86", "armeabi-v7a")); + + // Only 64-bit architectures. + assertThrows( + CommandExecutionException.class, + () -> matcher.checkDeviceCompatible(multiAbiTargeting(X86_64))); + assertThrows( + CommandExecutionException.class, + () -> matcher.checkDeviceCompatible(multiAbiTargeting(setOf(setOf(X86_64, ARM64_V8A))))); + assertThrows( + CommandExecutionException.class, + () -> + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(ARM64_V8A), setOf(X86_64))))); + assertThrows( + CommandExecutionException.class, + () -> matcher.checkDeviceCompatible(multiAbiTargeting(ARM64_V8A, setOf(X86_64)))); + // No set is fully contained in (x86, armeabi-v7a). + assertThrows( + CommandExecutionException.class, + () -> + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(X86_64, X86), setOf(ARM64_V8A))))); + assertThrows( + CommandExecutionException.class, + () -> + matcher.checkDeviceCompatible( + multiAbiTargeting(setOf(setOf(X86_64, X86, ARM64_V8A, ARMEABI_V7A))))); + } + + @Test + public void matchesTargeting_noMultiAbiTargeting() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86")); + + assertThat(matcher.matchesTargeting(MultiAbiTargeting.getDefaultInstance())).isTrue(); + } + + @Test + public void matchesTargeting_singleAbiDevice() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86")); + + assertThat(matcher.matchesTargeting(multiAbiTargeting(X86))).isTrue(); + assertThat(matcher.matchesTargeting(multiAbiTargeting(setOf(setOf(X86), setOf(ARMEABI_V7A))))) + .isTrue(); + assertThat(matcher.matchesTargeting(multiAbiTargeting(X86, setOf(X86_64)))).isTrue(); + assertThat(matcher.matchesTargeting(multiAbiTargeting(ARMEABI_V7A, setOf(X86)))).isFalse(); + assertThat(matcher.matchesTargeting(multiAbiTargeting(setOf(setOf(X86, ARMEABI_V7A))))) + .isFalse(); + } + + @Test + public void matchesTargeting_multipleAbiDevice_noMatch() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86_64", "x86", "arm64-v8a")); + + assertThat(matcher.matchesTargeting(multiAbiTargeting(ARMEABI_V7A, setOf(X86)))).isFalse(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86, ARMEABI_V7A)), setOf(setOf(X86))))) + .isFalse(); + } + + @Test + public void matchesTargeting_multipleAbiDevice_worseAlternatives() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86_64", "x86", "arm64-v8a")); + + assertThat(matcher.matchesTargeting(multiAbiTargeting(X86))).isTrue(); + assertThat(matcher.matchesTargeting(multiAbiTargeting(X86, setOf(ARM64_V8A)))).isTrue(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86_64, X86)), setOf(setOf(ARM64_V8A, ARMEABI_V7A))))) + .isTrue(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86_64)), setOf(setOf(X86, ARM64_V8A))))) + .isTrue(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86_64, X86)), setOf(setOf(X86_64, ARM64_V8A))))) + .isTrue(); + } + + @Test + public void matchesTargeting_multipleAbiDevice_betterAlternatives() { + MultiAbiMatcher matcher = new MultiAbiMatcher(lDeviceWithAbis("x86_64", "x86", "arm64-v8a")); + + assertThat(matcher.matchesTargeting(multiAbiTargeting(X86, setOf(X86_64)))).isFalse(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(ARM64_V8A, ARMEABI_V7A)), setOf(setOf(X86_64, X86))))) + .isFalse(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86)), setOf(setOf(X86_64, ARM64_V8A))))) + .isFalse(); + assertThat( + matcher.matchesTargeting( + multiAbiTargeting(setOf(setOf(X86_64, ARM64_V8A)), setOf(setOf(X86_64, X86))))) + .isFalse(); + } + + @SafeVarargs + private static ImmutableSet setOf(E... elements) { + return ImmutableSet.copyOf(elements); + } +} 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 f239f9cc..53b28d05 100755 --- a/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/VariantMatcherTest.java @@ -18,11 +18,13 @@ import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.HDPI; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.MDPI; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.XXXHDPI; import static com.android.tools.build.bundletool.testing.ApkSetUtils.splitApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.multiAbiTargetingStandaloneVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.splitApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.standaloneVariant; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; @@ -34,16 +36,21 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; 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.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 org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.Variant; import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.tools.build.bundletool.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -147,7 +154,6 @@ public void getAllMatchingVariants_partialDeviceSpec() { @Test public void getAllMatchingVariants_fullDeviceSpec() { ZipPath standaloneX86MdpiApk = ZipPath.create("standalone-x86.mdpi.apk"); - ZipPath.create("standalone-x86.xxxhdpi.apk"); ZipPath baseMasterSplitApk = ZipPath.create("base-master.apk"); ZipPath baseArmSplitApk = ZipPath.create("base-arm.apk"); ZipPath screenXxxhdpiApk = ZipPath.create("screen-xxxhdpi.apk"); @@ -193,4 +199,68 @@ public void getAllMatchingVariants_fullDeviceSpec() { assertThat(new VariantMatcher(postLDevice).getAllMatchingVariants(buildApksResult)) .containsExactly(splitVariant); } + + @Test + public void getAllMatchingVariants_apexVariants_noMatch_throws() { + ZipPath x86Apk = ZipPath.create("standalone-x86.apk"); + ZipPath x64X86Apk = ZipPath.create("standalone-x86_64.x86.apk"); + + ImmutableSet> x86Set = ImmutableSet.of(ImmutableSet.of(X86)); + ImmutableSet> x64X86Set = ImmutableSet.of(ImmutableSet.of(X86_64, X86)); + + MultiAbiTargeting x86Targeting = multiAbiTargeting(x86Set, x64X86Set); + MultiAbiTargeting x64X86Targeting = multiAbiTargeting(x64X86Set, x86Set); + + Variant x86Variant = multiAbiTargetingStandaloneVariant(x86Targeting, x86Apk); + Variant x64X86Variant = multiAbiTargetingStandaloneVariant(x64X86Targeting, x64X86Apk); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addAllVariant(ImmutableList.of(x86Variant, x64X86Variant)) + .build(); + + CommandExecutionException e = + assertThrows( + CommandExecutionException.class, + () -> + new VariantMatcher(abis("x86_64", "armeabi-v7a")) + .getAllMatchingVariants(buildApksResult)); + assertThat(e) + .hasMessageThat() + .contains( + "No set of ABI architectures that the app supports is contained in the ABI " + + "architecture set of the device"); + } + + @Test + public void getAllMatchingVariants_apexVariants_fullDeviceSpec() { + ZipPath x86Apk = ZipPath.create("standalone-x86.apk"); + ZipPath x64X86Apk = ZipPath.create("standalone-x86_64.x86.apk"); + + ImmutableSet> x86Set = ImmutableSet.of(ImmutableSet.of(X86)); + ImmutableSet> x64X86Set = ImmutableSet.of(ImmutableSet.of(X86_64, X86)); + + MultiAbiTargeting x86Targeting = multiAbiTargeting(x86Set, x64X86Set); + MultiAbiTargeting x64X86Targeting = multiAbiTargeting(x64X86Set, x86Set); + + Variant x86Variant = multiAbiTargetingStandaloneVariant(x86Targeting, x86Apk); + Variant x64X86Variant = multiAbiTargetingStandaloneVariant(x64X86Targeting, x64X86Apk); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .addAllVariant(ImmutableList.of(x86Variant, x64X86Variant)) + .build(); + + assertThat(new VariantMatcher(abis("x86")).getAllMatchingVariants(buildApksResult)) + .containsExactly(x86Variant); + assertThat(new VariantMatcher(abis("x86_64", "x86")).getAllMatchingVariants(buildApksResult)) + .containsExactly(x64X86Variant); + assertThat( + new VariantMatcher(abis("x86_64", "x86", "armeabi-v7a")) + .getAllMatchingVariants(buildApksResult)) + .containsExactly(x64X86Variant); + // Other device specs don't affect the matching variant. + assertThat( + new VariantMatcher(mergeSpecs(abis("x86"), density(HDPI))) + .getAllMatchingVariants(buildApksResult)) + .containsExactly(x86Variant); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/integration/BuildExtractApksTest.java b/src/test/java/com/android/tools/build/bundletool/integration/BuildExtractApksTest.java index 1dfb22a1..3302dea3 100755 --- a/src/test/java/com/android/tools/build/bundletool/integration/BuildExtractApksTest.java +++ b/src/test/java/com/android/tools/build/bundletool/integration/BuildExtractApksTest.java @@ -17,6 +17,7 @@ import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.HDPI; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.XHDPI; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.UNIVERSAL; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.density; import static com.android.tools.build.bundletool.testing.DeviceFactory.locales; @@ -151,7 +152,7 @@ public void forPreLDeviceUniversalApk() throws Exception { .setBundlePath(bundlePath) .setOutputFile(outputFilePath) .setAapt2Command(aapt2Command) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .build(); Path apkSetFilePath = command.execute(); @@ -183,7 +184,7 @@ public void forLDeviceUniversalApk() throws Exception { .setBundlePath(bundlePath) .setOutputFile(outputFilePath) .setAapt2Command(aapt2Command) - .setGenerateOnlyUniversalApk(true) + .setApkBuildMode(UNIVERSAL) .build(); Path apkSetFilePath = command.execute(); 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 7482f8e0..bd9161d7 100755 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java @@ -28,12 +28,14 @@ import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.value; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.nativeDirectoryTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.nativeLibraries; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedNativeDirectory; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; +import static com.google.common.collect.Iterables.getOnlyElement; 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; @@ -63,7 +65,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import java.nio.file.Path; @@ -111,12 +112,9 @@ public void merge_oneSetOfSplits_producesSingleShard() throws Exception { new ModuleSplitsToShardMerger(d8DexMerger, tmpDir) .merge(ImmutableList.of(ImmutableList.of(masterSplit, x86Split)), NO_MAIN_DEX_LIST); - assertThat(shards).hasSize(1); - ModuleSplit shard = shards.get(0); + ModuleSplit shard = getOnlyElement(shards); assertThat(shard.getApkTargeting()).isEqualTo(apkAbiTargeting(AbiAlias.X86)); - assertThat(shard.getVariantTargeting()).isEqualToDefaultInstance(); - assertThat(shard.getSplitType()).isEqualTo(SplitType.STANDALONE); - assertThat(extractPaths(shard.getEntries())).containsExactly("lib/x86/libtest.so"); + assertSingleEntryStandaloneShard(shard, "lib/x86/libtest.so"); } @Test @@ -148,14 +146,10 @@ public void merge_twoSetsOfSplits_producesTwoShards() throws Exception { assertThat(shards).hasSize(2); ImmutableMap shardsByTargeting = Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); - ModuleSplit x86Shard = shardsByTargeting.get(apkAbiTargeting(AbiAlias.X86)); - assertThat(x86Shard.getVariantTargeting()).isEqualToDefaultInstance(); - assertThat(x86Shard.getSplitType()).isEqualTo(SplitType.STANDALONE); - assertThat(extractPaths(x86Shard.getEntries())).containsExactly("lib/x86/libtest.so"); - ModuleSplit mipsShard = shardsByTargeting.get(apkAbiTargeting(AbiAlias.MIPS)); - assertThat(mipsShard.getVariantTargeting()).isEqualToDefaultInstance(); - assertThat(mipsShard.getSplitType()).isEqualTo(SplitType.STANDALONE); - assertThat(extractPaths(mipsShard.getEntries())).containsExactly("lib/mips/libtest.so"); + assertSingleEntryStandaloneShard( + shardsByTargeting.get(apkAbiTargeting(AbiAlias.X86)), "lib/x86/libtest.so"); + assertSingleEntryStandaloneShard( + shardsByTargeting.get(apkAbiTargeting(AbiAlias.MIPS)), "lib/mips/libtest.so"); } @Test @@ -254,9 +248,9 @@ public void dexFiles_inMultipleModules_areMerged() throws Exception { assertThat(mergedDexData).isNotEqualTo(TestData.readBytes("testdata/dex/classes-other.dex")); // The merged result should be cached. assertThat(dexMergingCache).hasSize(1); - ImmutableSet cacheKey = Iterables.getOnlyElement(dexMergingCache.keySet()); + ImmutableSet cacheKey = getOnlyElement(dexMergingCache.keySet()); assertThat(cacheKey).containsExactly(dexEntry1, dexEntry2); - ImmutableList cacheValue = Iterables.getOnlyElement(dexMergingCache.values()); + ImmutableList cacheValue = getOnlyElement(dexMergingCache.values()); assertThat(cacheValue.stream().allMatch(cachedFile -> cachedFile.startsWith(tmpDir))).isTrue(); } @@ -551,6 +545,100 @@ public void propagatesDebuggable_trueWhenSetToTrue() throws Exception { verifyNoMoreInteractions(spyDexMerger); } + @Test + public void mergeApex_oneSetOfSplits_producesOneShard() throws Exception { + ModuleSplit masterSplit = createModuleSplitBuilder().setMasterSplit(true).build(); + ModuleSplit x86Split = + createModuleSplitBuilder() + .setEntries(ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86.img", DUMMY_CONTENT))) + .setMasterSplit(false) + .setApkTargeting(apkMultiAbiTargeting(AbiAlias.X86)) + .build(); + + ImmutableList shards = + new ModuleSplitsToShardMerger(d8DexMerger, tmpDir) + .mergeApex(ImmutableList.of(ImmutableList.of(masterSplit, x86Split))); + + ModuleSplit x86Shard = getOnlyElement(shards); + assertThat(x86Shard.getApkTargeting()).isEqualTo(apkMultiAbiTargeting(AbiAlias.X86)); + assertSingleEntryStandaloneShard(x86Shard, "apex/x86.img"); + } + + @Test + public void mergeApex_twoSetsOfSplits_producesTwoShards() throws Exception { + ModuleSplit masterSplit = createModuleSplitBuilder().setMasterSplit(true).build(); + ModuleSplit x86Split = + createModuleSplitBuilder() + .setEntries(ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86.img", DUMMY_CONTENT))) + .setMasterSplit(false) + .setApkTargeting(apkMultiAbiTargeting(AbiAlias.X86)) + .build(); + ModuleSplit mipsSplit = + createModuleSplitBuilder() + .setEntries( + ImmutableList.of(InMemoryModuleEntry.ofFile("apex/mips.img", DUMMY_CONTENT))) + .setMasterSplit(false) + .setApkTargeting(apkMultiAbiTargeting(AbiAlias.MIPS)) + .build(); + + ImmutableList shards = + new ModuleSplitsToShardMerger(d8DexMerger, tmpDir) + .mergeApex( + ImmutableList.of( + ImmutableList.of(masterSplit, x86Split), + ImmutableList.of(masterSplit, mipsSplit))); + + assertThat(shards).hasSize(2); + ImmutableMap shardsByTargeting = + Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); + + assertSingleEntryStandaloneShard( + shardsByTargeting.get(apkMultiAbiTargeting(AbiAlias.X86)), "apex/x86.img"); + assertSingleEntryStandaloneShard( + shardsByTargeting.get(apkMultiAbiTargeting(AbiAlias.MIPS)), "apex/mips.img"); + } + + @Test + public void mergeApex_twoSetsOfSplits_multipleAbi_producesTwoShards() throws Exception { + ApkTargeting singleAbiTargeting = + apkMultiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64)), + ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64, AbiAlias.X86))); + ApkTargeting doubleAbiTargeting = + apkMultiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64, AbiAlias.X86)), + ImmutableSet.of(ImmutableSet.of(AbiAlias.X86_64))); + ModuleSplit masterSplit = createModuleSplitBuilder().setMasterSplit(true).build(); + ModuleSplit singleAbiSplit = + createModuleSplitBuilder() + .setEntries( + ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86_64.img", DUMMY_CONTENT))) + .setMasterSplit(false) + .setApkTargeting(singleAbiTargeting) + .build(); + ModuleSplit doubleAbiSplit = + createModuleSplitBuilder() + .setEntries( + ImmutableList.of(InMemoryModuleEntry.ofFile("apex/x86_64.x86.img", DUMMY_CONTENT))) + .setMasterSplit(false) + .setApkTargeting(doubleAbiTargeting) + .build(); + + ImmutableList shards = + new ModuleSplitsToShardMerger(d8DexMerger, tmpDir) + .mergeApex( + ImmutableList.of( + ImmutableList.of(masterSplit, doubleAbiSplit), + ImmutableList.of(masterSplit, singleAbiSplit))); + + assertThat(shards).hasSize(2); + ImmutableMap shardsByTargeting = + Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); + assertSingleEntryStandaloneShard(shardsByTargeting.get(singleAbiTargeting), "apex/x86_64.img"); + assertSingleEntryStandaloneShard( + shardsByTargeting.get(doubleAbiTargeting), "apex/x86_64.x86.img"); + } + /** Creates {@link ModuleSplit.Builder} with fields pre-populated to default values. */ private ModuleSplit.Builder createModuleSplitBuilder() { return ModuleSplit.builder() @@ -565,4 +653,10 @@ private ModuleSplit.Builder createModuleSplitBuilder() { private static Map, ImmutableList> createCache() { return new HashMap<>(); } + + private static void assertSingleEntryStandaloneShard(ModuleSplit shard, String entry) { + assertThat(shard.getVariantTargeting()).isEqualToDefaultInstance(); + assertThat(shard.getSplitType()).isEqualTo(SplitType.STANDALONE); + assertThat(extractPaths(shard.getEntries())).containsExactly(entry); + } } 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 5a215283..84308f58 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java @@ -17,10 +17,10 @@ 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.TargetingUtils.abi; 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.targetedNativeDirectory; +import static com.android.tools.build.bundletool.testing.TargetingUtils.toAbi; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; @@ -307,7 +307,7 @@ public void targetedAbis_abiInAllModules() throws Exception { .build(); assertThat(appBundle.getTargetedAbis()) - .containsExactly(abi(AbiAlias.ARMEABI), abi(AbiAlias.X86_64)); + .containsExactly(toAbi(AbiAlias.ARMEABI), toAbi(AbiAlias.X86_64)); } @Test @@ -335,7 +335,40 @@ public void targetedAbis_abiInSomeModules() throws Exception { .build(); assertThat(appBundle.getTargetedAbis()) - .containsExactly(abi(AbiAlias.X86), abi(AbiAlias.ARM64_V8A)); + .containsExactly(toAbi(AbiAlias.X86), toAbi(AbiAlias.ARM64_V8A)); + } + + @Test + public void renderscript_bcFilesPresent() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + baseModule -> + baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + .addModule( + "detail", + module -> module.setManifest(MANIFEST).addFile("res/raw/script.bc", DUMMY_CONTENT)) + .build(); + + assertThat(appBundle.has32BitRenderscriptCode()).isTrue(); + } + + @Test + public void renderscript_bcFilesAbsent() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + baseModule -> + baseModule.setManifest(MANIFEST).addFile("dex/classes.dex", DUMMY_CONTENT)) + .addModule( + "detail", + module -> + module.setManifest(MANIFEST).addFile("assets/language.pak", DUMMY_CONTENT)) + .build(); + + assertThat(appBundle.has32BitRenderscriptCode()).isFalse(); } private static ZipBuilder createBasicZipBuilder(BundleConfig config) { 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 0ccf909f..e10c4493 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java @@ -62,6 +62,7 @@ public void setUp() { } private static final BundleConfig DEFAULT_BUNDLE_CONFIG = BundleConfigBuilder.create().build(); + private static final byte[] DUMMY_CONTENT = new byte[0]; @Test public void missingAssetsProtoFile_returnsEmptyProto() { @@ -288,9 +289,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 = InMemoryModuleEntry.ofFile("dir1/entry1", new byte[0]); - ModuleEntry entry2 = InMemoryModuleEntry.ofFile("dir1/entry2", new byte[0]); - ModuleEntry entry3 = InMemoryModuleEntry.ofFile("dir1longer/entry3", new byte[0]); + ModuleEntry entry1 = InMemoryModuleEntry.ofFile("dir1/entry1", DUMMY_CONTENT); + ModuleEntry entry2 = InMemoryModuleEntry.ofFile("dir1/entry2", DUMMY_CONTENT); + ModuleEntry entry3 = InMemoryModuleEntry.ofFile("dir1longer/entry3", DUMMY_CONTENT); BundleModule bundleModule = createMinimalModuleBuilder().addEntries(Arrays.asList(entry1, entry2, entry3)).build(); @@ -301,7 +302,7 @@ public void entriesUnderPath_withPrefixDirectory() throws Exception { @Test public void getEntry_existing_found() throws Exception { - ModuleEntry entry = InMemoryModuleEntry.ofFile("dir/entry", new byte[0]); + ModuleEntry entry = InMemoryModuleEntry.ofFile("dir/entry", DUMMY_CONTENT); BundleModule bundleModule = createMinimalModuleBuilder().addEntries(Arrays.asList(entry)).build(); @@ -417,6 +418,29 @@ public void getdeliveryType_installTimeElement_noConditions() throws Exception { assertThat(bundleModule.getDeliveryType()).isEqualTo(ModuleDeliveryType.ALWAYS_INITIAL_INSTALL); } + @Test + public void renderscriptFiles_present() throws Exception { + BundleModule bundleModule = + createMinimalModuleBuilder() + .setAndroidManifestProto(androidManifest("com.test.app")) + .addEntry(InMemoryModuleEntry.ofFile("dex/classes.dex", DUMMY_CONTENT)) + .addEntry(InMemoryModuleEntry.ofFile("res/raw/yuv2rgb.bc", DUMMY_CONTENT)) + .build(); + + assertThat(bundleModule.hasRenderscript32Bitcode()).isTrue(); + } + + @Test + public void renderscriptFiles_absent() throws Exception { + BundleModule bundleModule = + createMinimalModuleBuilder() + .setAndroidManifestProto(androidManifest("com.test.app")) + .addEntry(InMemoryModuleEntry.ofFile("dex/classes.dex", DUMMY_CONTENT)) + .build(); + + assertThat(bundleModule.hasRenderscript32Bitcode()).isFalse(); + } + private static BundleModule.Builder createMinimalModuleBuilder() { return BundleModule.builder() .setName(BundleModuleName.create("testModule")) diff --git a/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java b/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java index 775255c4..4fbdff67 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/GeneratedApksTest.java @@ -37,6 +37,7 @@ public void builder_empty() { assertThat(generatedApks.getSplitApks()).isEmpty(); assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); + assertThat(generatedApks.getSystemApks()).isEmpty(); } @Test @@ -46,6 +47,7 @@ public void fromModuleSplits_empty() { assertThat(generatedApks.getSplitApks()).isEmpty(); assertThat(generatedApks.getStandaloneApks()).isEmpty(); assertThat(generatedApks.getInstantApks()).isEmpty(); + assertThat(generatedApks.getSystemApks()).isEmpty(); } @Test @@ -60,6 +62,19 @@ public void fromModuleSplits_correctSizes() { assertThat(generatedApks.getSplitApks()).containsExactly(splitApk); assertThat(generatedApks.getStandaloneApks()).containsExactly(standaloneApk); assertThat(generatedApks.getInstantApks()).containsExactly(instantApk); + assertThat(generatedApks.getSystemApks()).isEmpty(); + } + + @Test + public void fromModuleSplits_withSystemSplits_correctSizes() { + ModuleSplit systemApk = createModuleSplit(SplitType.SYSTEM); + + GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(ImmutableList.of(systemApk)); + assertThat(generatedApks.size()).isEqualTo(1); + assertThat(generatedApks.getSplitApks()).isEmpty(); + assertThat(generatedApks.getStandaloneApks()).isEmpty(); + assertThat(generatedApks.getInstantApks()).isEmpty(); + assertThat(generatedApks.getSystemApks()).containsExactly(systemApk); } private static ModuleSplit createModuleSplit(SplitType splitType) { 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 eea6e45d..4f858a6e 100755 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -31,10 +31,12 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkGraphicsTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.graphicsApiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.multiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.openGlVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.vulkanVersionFrom; @@ -53,6 +55,7 @@ import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.bundle.Targeting.ScreenDensityTargeting; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; +import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoNode; @@ -319,6 +322,28 @@ public void moduleAbiSplitSuffixAndName_alternatives() { assertThat(resSplit.getAndroidManifest().getSplitId()).hasValue("config.other_abis"); } + @Test + public void apexModuleMultiAbiSplitSuffixAndName() { + ModuleSplit resSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setEntries(ImmutableList.of()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setApkTargeting( + apkMultiAbiTargeting( + multiAbiTargeting( + ImmutableSet.of( + ImmutableSet.of(AbiAlias.X86_64, AbiAlias.X86), + ImmutableSet.of(AbiAlias.ARM64_V8A, AbiAlias.ARMEABI_V7A)), + ImmutableSet.of()))) + .setMasterSplit(false) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + resSplit = resSplit.writeSplitIdInManifest(resSplit.getSuffix()); + assertThat(resSplit.getAndroidManifest().getSplitId()) + .hasValue("config.x86_64.x86_arm64_v8a.armeabi_v7a"); + } + @Test public void moduleLanguageSplitSuffixAndName() { ModuleSplit langSplit = diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java new file mode 100755 index 00000000..bd3d1b3f --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AbiApexImagesSplitterTest.java @@ -0,0 +1,179 @@ +/* + * 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.splitters; + +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImages; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargetingFromAllTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedApexImage; +import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import com.android.bundle.Files.ApexImages; +import com.android.bundle.Targeting.Abi.AbiAlias; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the AbiApexImagesSplitter class. */ +@RunWith(JUnit4.class) +public class AbiApexImagesSplitterTest { + + @Test + public void splittingBySingleAbi_oneImageFile() throws Exception { + AbiApexImagesSplitter abiApexImagesSplitter = new AbiApexImagesSplitter(); + ImmutableCollection splits = + abiApexImagesSplitter.split( + ModuleSplit.forApex(createSingleImageModule("testModule", "x86"))); + + ModuleSplit x86Split = Iterables.getOnlyElement(splits.asList()); + assertThat(x86Split.getApkTargeting()).isEqualTo(apkMultiAbiTargeting(X86)); + assertThat(x86Split.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); + assertThat(extractPaths(x86Split.getEntries())).containsExactly("apex/x86.img"); + } + + @Test + public void splittingBySingleAbi_twoImageFiles() throws Exception { + ApexImages apexConfig = + apexImages( + targetedApexImage("apex/x86.img", apexImageTargeting("x86")), + targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64"))); + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .addFile("apex/x86.img") + .addFile("apex/x86_64.img") + .setApexConfig(apexConfig) + .setManifest(androidManifest("com.test.app")) + .build(); + + AbiApexImagesSplitter abiApexImagesSplitter = new AbiApexImagesSplitter(); + ImmutableCollection splits = + abiApexImagesSplitter.split(ModuleSplit.forApex(bundleModule)); + + assertThat(splits).hasSize(2); + assertThat(splits.stream().map(ModuleSplit::getVariantTargeting).collect(toImmutableSet())) + .containsExactly(lPlusVariantTargeting()); + ApkTargeting x86Targeting = apkMultiAbiTargeting(X86, ImmutableSet.of(X86_64)); + ApkTargeting x64Targeting = apkMultiAbiTargeting(X86_64, ImmutableSet.of(X86)); + ImmutableMap splitsByTargeting = + Maps.uniqueIndex(splits, ModuleSplit::getApkTargeting); + assertThat(splitsByTargeting.keySet()).containsExactly(x86Targeting, x64Targeting); + assertThat(extractPaths(splitsByTargeting.get(x86Targeting).getEntries())) + .containsExactly("apex/x86.img"); + assertThat(extractPaths(splitsByTargeting.get(x64Targeting).getEntries())) + .containsExactly("apex/x86_64.img"); + } + + @Test + public void splittingByMultipleAbi_multipleImageFiles() throws Exception { + ApexImages apexConfig = + apexImages( + targetedApexImage("apex/x86_64.x86.img", apexImageTargeting("x86_64", "x86")), + targetedApexImage( + "apex/x86_64.armeabi-v7a.img", apexImageTargeting("x86_64", "armeabi-v7a")), + targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64")), + targetedApexImage("apex/x86.armeabi-v7a.img", apexImageTargeting("x86", "armeabi-v7a")), + targetedApexImage("apex/x86.img", apexImageTargeting("x86")), + targetedApexImage("apex/armeabi-v7a.img", apexImageTargeting("armeabi-v7a"))); + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .addFile("apex/x86_64.x86.img") + .addFile("apex/x86_64.armeabi-v7a.img") + .addFile("apex/x86_64.img") + .addFile("apex/x86.armeabi-v7a.img") + .addFile("apex/x86.img") + .addFile("apex/armeabi-v7a.img") + .setApexConfig(apexConfig) + .setManifest(androidManifest("com.test.app")) + .build(); + + AbiApexImagesSplitter abiApexImagesSplitter = new AbiApexImagesSplitter(); + ImmutableCollection splits = + abiApexImagesSplitter.split(ModuleSplit.forApex(bundleModule)); + + assertThat(splits).hasSize(6); + assertThat(splits.stream().map(ModuleSplit::getVariantTargeting).collect(toImmutableSet())) + .containsExactly(lPlusVariantTargeting()); + ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); + ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64Set = ImmutableSet.of(X86_64); + ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86Set = ImmutableSet.of(X86); + ImmutableSet armSet = ImmutableSet.of(ARMEABI_V7A); + ImmutableSet> allTargeting = + ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, armSet); + ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); + ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); + ApkTargeting a64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); + ApkTargeting x86ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x86ArmSet, allTargeting); + ApkTargeting x86Targeting = apkMultiAbiTargetingFromAllTargeting(x86Set, allTargeting); + ApkTargeting armTargeting = apkMultiAbiTargetingFromAllTargeting(armSet, allTargeting); + ImmutableMap splitsByTargeting = + Maps.uniqueIndex(splits, ModuleSplit::getApkTargeting); + assertThat(splitsByTargeting.keySet()) + .containsExactly( + x64X86Targeting, + x64ArmTargeting, + a64Targeting, + x86ArmTargeting, + x86Targeting, + armTargeting); + assertThat(extractPaths(splitsByTargeting.get(x64X86Targeting).getEntries())) + .containsExactly("apex/x86_64.x86.img"); + assertThat(extractPaths(splitsByTargeting.get(x64ArmTargeting).getEntries())) + .containsExactly("apex/x86_64.armeabi-v7a.img"); + assertThat(extractPaths(splitsByTargeting.get(a64Targeting).getEntries())) + .containsExactly("apex/x86_64.img"); + assertThat(extractPaths(splitsByTargeting.get(x86ArmTargeting).getEntries())) + .containsExactly("apex/x86.armeabi-v7a.img"); + assertThat(extractPaths(splitsByTargeting.get(x86Targeting).getEntries())) + .containsExactly("apex/x86.img"); + assertThat(extractPaths(splitsByTargeting.get(armTargeting).getEntries())) + .containsExactly("apex/armeabi-v7a.img"); + } + + /** Creates a minimal module with one apex image file targeted at the given cpu architecture. */ + private static BundleModule createSingleImageModule(String moduleName, String architecture) + throws Exception { + String relativeFilePath = "apex/" + architecture + ".img"; + ApexImages apexConfig = + apexImages(targetedApexImage(relativeFilePath, apexImageTargeting(architecture))); + + return new BundleModuleBuilder(moduleName) + .addFile(relativeFilePath) + .setApexConfig(apexConfig) + .setManifest(androidManifest("com.test.app")) + .build(); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitterTest.java index 38f45f6e..ff802259 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AbiNativeLibrariesSplitterTest.java @@ -16,6 +16,8 @@ package com.android.tools.build.bundletool.splitters; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.tools.build.bundletool.model.ManifestMutator.withSplitsRequired; @@ -30,10 +32,12 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Files.NativeLibraries; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; +import com.android.tools.build.bundletool.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; @@ -63,6 +67,64 @@ public void splittingByAbi() throws Exception { assertThat(extractPaths(x86Split.getEntries())).containsExactly("lib/x86/libnoname.so"); } + @Test + public void splittingByMultipleAbis_64BitDisabled() throws Exception { + NativeLibraries nativeConfig = + nativeLibraries( + targetedNativeDirectory("lib/armeabi-v7a", nativeDirectoryTargeting(ARMEABI_V7A)), + targetedNativeDirectory("lib/arm64-v8a", nativeDirectoryTargeting(ARM64_V8A)), + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64))); + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .setNativeConfig(nativeConfig) + .addFile("lib/armeabi-v7a/libtest.so") + .addFile("lib/arm64-v8a/libtest.so") + .addFile("lib/x86_64/libtest.so") + .setManifest(androidManifest("com.test.app")) + .build(); + + AbiNativeLibrariesSplitter abiNativeLibrariesSplitter = + new AbiNativeLibrariesSplitter(/* include64BitLibs= */ false); + + ImmutableCollection splits = + abiNativeLibrariesSplitter.split(ModuleSplit.forNativeLibraries(bundleModule)); + + assertThat(splits).hasSize(1); + ModuleSplit abiSplit = splits.asList().get(0); + assertThat(abiSplit.getApkTargeting()) + .isEqualTo(apkAbiTargeting(ARMEABI_V7A, ImmutableSet.of())); + assertThat(extractPaths(abiSplit.getEntries())).containsExactly("lib/armeabi-v7a/libtest.so"); + } + + @Test + public void splittingByMultipleAbis_64BitDisabled_no32BitLibs_throws() throws Exception { + NativeLibraries nativeConfig = + nativeLibraries( + targetedNativeDirectory("lib/arm64-v8a", nativeDirectoryTargeting(ARM64_V8A)), + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64))); + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .setNativeConfig(nativeConfig) + .addFile("lib/arm64-v8a/libtest.so") + .addFile("lib/x86_64/libtest.so") + .setManifest(androidManifest("com.test.app")) + .build(); + + AbiNativeLibrariesSplitter abiNativeLibrariesSplitter = + new AbiNativeLibrariesSplitter(/* include64BitLibs= */ false); + + CommandExecutionException exception = + assertThrows( + CommandExecutionException.class, + () -> abiNativeLibrariesSplitter.split(ModuleSplit.forNativeLibraries(bundleModule))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Generation of 64-bit native libraries is " + + "disabled, but App Bundle contains only 64-bit native libraries."); + } + @Test public void splittingByMultipleAbis() throws Exception { NativeLibraries nativeConfig = diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AbiPlaceholderInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AbiPlaceholderInjectorTest.java index a2860011..dd36ceff 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AbiPlaceholderInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AbiPlaceholderInjectorTest.java @@ -17,7 +17,7 @@ package com.android.tools.build.bundletool.splitters; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; -import static com.android.tools.build.bundletool.testing.TargetingUtils.abi; +import static com.android.tools.build.bundletool.testing.TargetingUtils.toAbi; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; import static com.google.common.truth.Truth.assertThat; @@ -39,7 +39,7 @@ public void addPlaceholderNativeEntries_newEntriesAdded() throws Exception { new BundleModuleBuilder("base").setManifest(androidManifest("com.test")).build()); AbiPlaceholderInjector abiPlaceholderInjector = - new AbiPlaceholderInjector(ImmutableSet.of(abi(AbiAlias.ARMEABI_V7A))); + new AbiPlaceholderInjector(ImmutableSet.of(toAbi(AbiAlias.ARMEABI_V7A))); ModuleSplit actualModuleSplit = abiPlaceholderInjector.addPlaceholderNativeEntries(moduleSplit); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java index 3cf69c5e..932ac599 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/BundleSharderTest.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.splitters; import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.tools.build.bundletool.model.AndroidManifest.ACTIVITY_ELEMENT_NAME; @@ -35,13 +36,17 @@ 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.android.tools.build.bundletool.testing.TargetingUtils.apexImageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apexImages; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargetingFromAllTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assets; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.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.targetedApexImage; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedNativeDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; @@ -59,6 +64,7 @@ import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.XmlElement; import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Files.ApexImages; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; @@ -84,10 +90,13 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(Theories.class) public class BundleSharderTest { private static final BundleMetadata DEFAULT_METADATA = BundleMetadata.builder().build(); @@ -129,14 +138,20 @@ public class BundleSharderTest { private BundleSharder bundleSharder; private Path tmpDir; + @DataPoints("generate64BitShards") + public static final ImmutableSet GENERATE_64_BIT_LIBS = ImmutableSet.of(true, false); + @Before public void setUp() { tmpDir = Paths.get("real/directory/not/needed/in/this/test"); bundleSharder = new BundleSharder(tmpDir, BundleToolVersion.getCurrentVersion()); } + /** If no dimension is set, the generate-64-bits-native-libs has no effect. */ @Test - public void shardByNoDimension_producesOneApk() throws Exception { + @Theory + public void shardByNoDimension_producesOneApk( + @FromDataPoints("generate64BitShards") boolean generate64BitShards) throws Exception { BundleModule bundleModule = new BundleModuleBuilder("base") .addFile("assets/file.txt") @@ -158,6 +173,9 @@ public void shardByNoDimension_producesOneApk() throws Exception { fileReference("res/drawable-mdpi/image.jpg", MDPI))) .build(); + BundleSharder bundleSharder = + new BundleSharder(tmpDir, BundleToolVersion.getCurrentVersion(), generate64BitShards); + ImmutableList shards = bundleSharder.shardBundle( ImmutableList.of(bundleModule), ImmutableSet.of(), DEFAULT_METADATA); @@ -367,6 +385,39 @@ public void shardByAbi_havingManyAbis_producesManyApks() throws Exception { .containsNoneOf("lib/armeabi/libtest.so", "lib/x86/libtest.so"); } + @Test + public void shardByAbi_64BitAbisDisabled_filteredOut() throws Exception { + BundleModule bundleModule = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .addFile("lib/x86/libtest.so") + .addFile("lib/x86_64/libtest.so") + .setManifest(androidManifest("com.test.app")) + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64)))) + .build(); + + BundleSharder bundleSharder = + new BundleSharder( + tmpDir, BundleToolVersion.getCurrentVersion(), /* generate64BitShards= */ false); + + ImmutableList shards = + bundleSharder.shardBundle( + ImmutableList.of(bundleModule), + ImmutableSet.of(OptimizationDimension.ABI), + DEFAULT_METADATA); + + assertThat(shards).hasSize(1); + ModuleSplit shard = shards.get(0); + assertThat(shard.getSplitType()).isEqualTo(SplitType.STANDALONE); + assertThat(shard.getVariantTargeting()).isEqualToDefaultInstance(); + assertThat(extractPaths(shard.getEntries())) + .containsAllOf("dex/classes.dex", "lib/x86/libtest.so"); + assertThat(shard.getApkTargeting()).isEqualTo(apkAbiTargeting(X86)); + } + @Test public void shardByAbi_assetsAbiTargetingIsIgnored() throws Exception { BundleModule bundleModule = @@ -418,6 +469,79 @@ public void shardByAbi_assetsAbiTargetingIsIgnored() throws Exception { "root/license.dat"); } + @Test + public void shardApexModule() throws Exception { + ApexImages apexConfig = + apexImages( + targetedApexImage("apex/x86_64.x86.img", apexImageTargeting("x86_64", "x86")), + targetedApexImage( + "apex/x86_64.armeabi-v7a.img", apexImageTargeting("x86_64", "armeabi-v7a")), + targetedApexImage("apex/x86_64.img", apexImageTargeting("x86_64")), + targetedApexImage("apex/x86.armeabi-v7a.img", apexImageTargeting("x86", "armeabi-v7a")), + targetedApexImage("apex/x86.img", apexImageTargeting("x86")), + targetedApexImage("apex/armeabi-v7a.img", apexImageTargeting("armeabi-v7a"))); + BundleModule apexModule = + new BundleModuleBuilder("base") + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.x86.img") + .addFile("apex/x86_64.armeabi-v7a.img") + .addFile("apex/x86_64.img") + .addFile("apex/x86.armeabi-v7a.img") + .addFile("apex/x86.img") + .addFile("apex/armeabi-v7a.img") + .setManifest(androidManifest("com.test.app")) + .setApexConfig(apexConfig) + .build(); + + ImmutableList shards = bundleSharder.shardApexBundle(apexModule); + + assertThat(shards).hasSize(6); + assertThat(shards.stream().map(ModuleSplit::getSplitType).distinct().collect(toImmutableSet())) + .containsExactly(SplitType.STANDALONE); + assertThat( + shards.stream() + .map(ModuleSplit::getVariantTargeting) + .distinct() + .collect(toImmutableSet())) + .containsExactly(VariantTargeting.getDefaultInstance()); + ImmutableSet x64X86Set = ImmutableSet.of(X86_64, X86); + ImmutableSet x64ArmSet = ImmutableSet.of(X86_64, ARMEABI_V7A); + ImmutableSet x64Set = ImmutableSet.of(X86_64); + ImmutableSet x86ArmSet = ImmutableSet.of(X86, ARMEABI_V7A); + ImmutableSet x86Set = ImmutableSet.of(X86); + ImmutableSet armSet = ImmutableSet.of(ARMEABI_V7A); + ImmutableSet> allTargeting = + ImmutableSet.of(x64X86Set, x64ArmSet, x64Set, x86ArmSet, x86Set, armSet); + ApkTargeting x64X86Targeting = apkMultiAbiTargetingFromAllTargeting(x64X86Set, allTargeting); + ApkTargeting x64ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x64ArmSet, allTargeting); + ApkTargeting a64Targeting = apkMultiAbiTargetingFromAllTargeting(x64Set, allTargeting); + ApkTargeting x86ArmTargeting = apkMultiAbiTargetingFromAllTargeting(x86ArmSet, allTargeting); + ApkTargeting x86Targeting = apkMultiAbiTargetingFromAllTargeting(x86Set, allTargeting); + ApkTargeting armTargeting = apkMultiAbiTargetingFromAllTargeting(armSet, allTargeting); + ImmutableMap splitsByTargeting = + Maps.uniqueIndex(shards, ModuleSplit::getApkTargeting); + assertThat(splitsByTargeting.keySet()) + .containsExactly( + x64X86Targeting, + x64ArmTargeting, + a64Targeting, + x86ArmTargeting, + x86Targeting, + armTargeting); + assertThat(extractPaths(splitsByTargeting.get(x64X86Targeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/x86_64.x86.img"); + assertThat(extractPaths(splitsByTargeting.get(x64ArmTargeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/x86_64.armeabi-v7a.img"); + assertThat(extractPaths(splitsByTargeting.get(a64Targeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/x86_64.img"); + assertThat(extractPaths(splitsByTargeting.get(x86ArmTargeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/x86.armeabi-v7a.img"); + assertThat(extractPaths(splitsByTargeting.get(x86Targeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/x86.img"); + assertThat(extractPaths(splitsByTargeting.get(armTargeting).getEntries())) + .containsExactly("root/apex_manifest.json", "apex/armeabi-v7a.img"); + } + @Test public void shardByDensity_havingNoResources_producesOneApk() throws Exception { BundleModule bundleModule = diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java index 55d8fa78..4545b92d 100755 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java @@ -46,7 +46,6 @@ import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.resourceTable; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.type; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.value; -import static com.android.tools.build.bundletool.testing.TargetingUtils.abi; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAlternativeLanguageTargeting; @@ -69,6 +68,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedNativeDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.toAbi; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMinSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; import static com.android.tools.build.bundletool.testing.truth.resources.TruthResourceTable.assertThat; @@ -693,7 +693,7 @@ public void nativeSplits_areGenerated() throws Exception { .addFile("lib/x86/liba.so") .build(); - List splits = createAbiAndDensitySplitter(testModule).splitModule(); + ImmutableList splits = createAbiAndDensitySplitter(testModule).splitModule(); assertThat(splits.stream().map(ModuleSplit::getSplitType).distinct().collect(toImmutableSet())) .containsExactly(SplitType.SPLIT); assertThat( @@ -724,6 +724,35 @@ public void nativeSplits_areGenerated() throws Exception { assertThat(hasX86Split).isTrue(); } + @Test + public void nativeSplits_64BitLibsDisabled() throws Exception { + NativeLibraries nativeConfig = + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting("x86")), + targetedNativeDirectory("lib/arm64-v8a", nativeDirectoryTargeting("arm64-v8a"))); + BundleModule testModule = + new BundleModuleBuilder("testModule") + .setManifest(androidManifest("com.test.app")) + .setNativeConfig(nativeConfig) + .addFile("lib/x86/liba.so") + .addFile("lib/arm64-v8a/liba.so") + .build(); + + ModuleSplitter moduleSplitter = + new ModuleSplitter( + testModule, BUNDLETOOL_VERSION, withDisabled64BitLibs(), lPlusVariantTargeting()); + + ImmutableList splits = moduleSplitter.splitModule(); + ImmutableMap toTargetingMap = + Maps.uniqueIndex(splits, ModuleSplit::getApkTargeting); + ApkTargeting x86SplitTargeting = + mergeApkTargeting(DEFAULT_MASTER_SPLIT_SDK_TARGETING, apkAbiTargeting(AbiAlias.X86)); + assertThat(toTargetingMap.keySet()) + .containsExactly(DEFAULT_MASTER_SPLIT_SDK_TARGETING, x86SplitTargeting); + assertThat(toTargetingMap.get(DEFAULT_MASTER_SPLIT_SDK_TARGETING).isMasterSplit()).isTrue(); + assertThat(toTargetingMap.get(x86SplitTargeting).isMasterSplit()).isFalse(); + } + @Test public void nativeSplits_lPlusTargeting_withAbiAndUncompressNativeLibsSplitter() throws Exception { @@ -1314,7 +1343,7 @@ public void addingLibraryPlaceholders_baseModule() throws Exception { BUNDLETOOL_VERSION, ApkGenerationConfiguration.builder() .setAbisForPlaceholderLibs( - ImmutableSet.of(abi(AbiAlias.X86), abi(AbiAlias.ARM64_V8A))) + ImmutableSet.of(toAbi(AbiAlias.X86), toAbi(AbiAlias.ARM64_V8A))) .build(), lPlusVariantTargeting()); @@ -1341,7 +1370,7 @@ public void addingLibraryPlaceholders_featureModule_noAction() throws Exception BUNDLETOOL_VERSION, ApkGenerationConfiguration.builder() .setAbisForPlaceholderLibs( - ImmutableSet.of(abi(AbiAlias.X86), abi(AbiAlias.ARM64_V8A))) + ImmutableSet.of(toAbi(AbiAlias.X86), toAbi(AbiAlias.ARM64_V8A))) .build(), lPlusVariantTargeting()); @@ -1387,4 +1416,11 @@ private static ApkGenerationConfiguration withOptimizationDimensions( .setOptimizationDimensions(optimizationDimensions) .build(); } + + private static ApkGenerationConfiguration withDisabled64BitLibs() { + return ApkGenerationConfiguration.builder() + .setInclude64BitLibs(false) + .setOptimizationDimensions(ImmutableSet.of(ABI)) + .build(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java new file mode 100755 index 00000000..91910742 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ShardedApksGeneratorTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017 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.splitters; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.LDPI; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.MDPI; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.createResourceTable; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.fileReference; +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.targetedNativeDirectory; +import static com.android.tools.build.bundletool.testing.TargetingUtils.toAbi; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMinSdkTargeting; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.Abi.AbiAlias; +import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.android.tools.build.bundletool.version.BundleToolVersion; +import com.android.tools.build.bundletool.version.Version; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Path; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class ShardedApksGeneratorTest { + private static final Version BUNDLETOOL_VERSION = BundleToolVersion.getCurrentVersion(); + private static final BundleMetadata DEFAULT_METADATA = BundleMetadata.builder().build(); + private static final ApkOptimizations DEFAULT_APK_OPTIMIZATIONS = + ApkOptimizations.getDefaultOptimizationsForVersion(BUNDLETOOL_VERSION); + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + private Path tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + } + + @DataPoints("standaloneSplitTypes") + public static final ImmutableSet STANDALONE_SPLIT_TYPES = + ImmutableSet.of(SplitType.STANDALONE, SplitType.SYSTEM); + + @Test + @Theory + public void simpleMultipleModules( + @FromDataPoints("standaloneSplitTypes") SplitType standaloneSplitType) throws Exception { + + ImmutableList bundleModule = + ImmutableList.of( + new BundleModuleBuilder("base") + .addFile("assets/leftover.txt") + .setManifest(androidManifest("com.test.app")) + .build(), + new BundleModuleBuilder("test") + .addFile("assets/test.txt") + .setManifest(androidManifest("com.test.app")) + .build()); + + ImmutableList moduleSplits = + new ShardedApksGenerator( + tmpDir, BUNDLETOOL_VERSION, standaloneSplitType, /* generate64BitShards= */ true) + .generateSplits(bundleModule, DEFAULT_METADATA, DEFAULT_APK_OPTIMIZATIONS); + + assertThat(moduleSplits).hasSize(1); + ModuleSplit moduleSplit = moduleSplits.get(0); + assertThat(moduleSplit.getSplitType()).isEqualTo(standaloneSplitType); + assertThat(getEntriesPaths(moduleSplit)) + .containsExactly("assets/test.txt", "assets/leftover.txt"); + assertThat(moduleSplit.getVariantTargeting()).isEqualTo(variantMinSdkTargeting(1)); + } + + @Test + @Theory + public void singleModule_withNativeLibsAndDensity( + @FromDataPoints("standaloneSplitTypes") SplitType standaloneSplitType) throws Exception { + + ImmutableList bundleModule = + ImmutableList.of( + new BundleModuleBuilder("base") + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + // Add some density-specific resources. + .addFile("res/drawable-ldpi/image.jpg") + .addFile("res/drawable-mdpi/image.jpg") + .setResourceTable( + createResourceTable( + "image", + fileReference("res/drawable-ldpi/image.jpg", LDPI), + fileReference("res/drawable-mdpi/image.jpg", MDPI))) + .setManifest(androidManifest("com.test.app")) + .build()); + + ImmutableList moduleSplits = + new ShardedApksGenerator( + tmpDir, BUNDLETOOL_VERSION, standaloneSplitType, /* generate64BitShards= */ true) + .generateSplits(bundleModule, DEFAULT_METADATA, DEFAULT_APK_OPTIMIZATIONS); + + assertThat(moduleSplits).hasSize(14); // 7 (density), 2 (abi) splits + assertThat(moduleSplits.stream().map(ModuleSplit::getSplitType).collect(toImmutableSet())) + .containsExactly(standaloneSplitType); + } + + @Test + @Theory + public void singleModule_withNativeLibsAndDensity_64bitNativeLibsDisabled( + @FromDataPoints("standaloneSplitTypes") SplitType standaloneSplitType) throws Exception { + + ImmutableList bundleModule = + ImmutableList.of( + new BundleModuleBuilder("base") + .addFile("lib/x86/libsome.so") + .addFile("lib/x86_64/libsome.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(AbiAlias.X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(AbiAlias.X86_64)))) + // Add some density-specific resources. + .addFile("res/drawable-ldpi/image.jpg") + .addFile("res/drawable-mdpi/image.jpg") + .setResourceTable( + createResourceTable( + "image", + fileReference("res/drawable-ldpi/image.jpg", LDPI), + fileReference("res/drawable-mdpi/image.jpg", MDPI))) + .setManifest(androidManifest("com.test.app")) + .build()); + + ImmutableList moduleSplits = + new ShardedApksGenerator( + tmpDir, BUNDLETOOL_VERSION, standaloneSplitType, /* generate64BitShards= */ false) + .generateSplits(bundleModule, DEFAULT_METADATA, DEFAULT_APK_OPTIMIZATIONS); + + assertThat(moduleSplits).hasSize(7); // 7 (density), 1 (abi) split + // Verify that the only ABI is x86. + ImmutableSet abiTargetings = + moduleSplits.stream() + .map(ModuleSplit::getApkTargeting) + .map(ApkTargeting::getAbiTargeting) + .map(AbiTargeting::getValueList) + .flatMap(List::stream) + .collect(toImmutableSet()); + assertThat(abiTargetings).containsExactly(toAbi(AbiAlias.X86)); + // And ABI has no alternatives. + ImmutableSet abiAlternatives = + moduleSplits.stream() + .map(ModuleSplit::getApkTargeting) + .map(ApkTargeting::getAbiTargeting) + .map(AbiTargeting::getAlternativesList) + .flatMap(List::stream) + .collect(toImmutableSet()); + assertThat(abiAlternatives).isEmpty(); + assertThat(moduleSplits.stream().map(ModuleSplit::getSplitType).collect(toImmutableSet())) + .containsExactly(standaloneSplitType); + } + + private static ImmutableSet getEntriesPaths(ModuleSplit moduleSplit) { + return moduleSplit.getEntries().stream() + .map(moduleEntry -> moduleEntry.getPath().toString()) + .collect(toImmutableSet()); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulatorTest.java b/src/test/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulatorTest.java index 5206e0f0..1838a667 100755 --- a/src/test/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/targeting/AlternativeVariantTargetingPopulatorTest.java @@ -145,6 +145,28 @@ public void instantPassThrough() throws Exception { assertThat(processedInstantSplits).isEqualTo(instantSplits); } + @Test + public void systemApksPassThrough() { + VariantTargeting emptySdkTargeting = variantSdkTargeting(SdkVersion.getDefaultInstance()); + ImmutableList systemSplits = + ImmutableList.of( + createModuleSplit( + mergeVariantTargeting( + emptySdkTargeting, variantDensityTargeting(DensityAlias.LDPI)), + SplitType.SYSTEM)); + + GeneratedApks generatedApks = GeneratedApks.builder().setSystemApks(systemSplits).build(); + + GeneratedApks processedApks = + AlternativeVariantTargetingPopulator.populateAlternativeVariantTargeting(generatedApks); + + assertThat(processedApks.size()).isEqualTo(1); + assertThat(processedApks.getInstantApks()).isEmpty(); + assertThat(processedApks.getStandaloneApks()).isEmpty(); + assertThat(processedApks.getSplitApks()).isEmpty(); + assertThat(processedApks.getSystemApks()).isEqualTo(systemSplits); + } + @Test public void abi_allVariantsAbiAgnostic_passThrough() throws Exception { ModuleSplit densityVariant = createModuleSplit(variantDensityTargeting(DensityAlias.LDPI)); diff --git a/src/test/java/com/android/tools/build/bundletool/targeting/TargetingComparatorsTest.java b/src/test/java/com/android/tools/build/bundletool/targeting/TargetingComparatorsTest.java new file mode 100755 index 00000000..9a4215cd --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/targeting/TargetingComparatorsTest.java @@ -0,0 +1,112 @@ +/* + * 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.targeting; + +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.MIPS64; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMultiAbiTargeting; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; + +import com.android.bundle.Targeting.Abi.AbiAlias; +import com.android.bundle.Targeting.VariantTargeting; +import com.google.common.collect.Comparators; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TargetingComparatorsTest { + + // Realistic sets of targeting architectures. + private static final ImmutableList> REALISTIC_TARGETING_LIST = + ImmutableList.of( + ImmutableSet.of(ARMEABI_V7A), + ImmutableSet.of(ARM64_V8A), + ImmutableSet.of(ARM64_V8A, ARMEABI_V7A), + ImmutableSet.of(X86), + ImmutableSet.of(X86, ARMEABI_V7A), + ImmutableSet.of(X86_64), + ImmutableSet.of(X86_64, ARMEABI_V7A), + ImmutableSet.of(X86_64, ARM64_V8A, ARMEABI_V7A), + ImmutableSet.of(X86_64, X86), + ImmutableSet.of(X86_64, X86, ARMEABI_V7A), + ImmutableSet.of(X86_64, X86, ARM64_V8A, ARMEABI_V7A)); + + // All different combinations of three architectures. + private static final ImmutableList> EXHAUSTIVE_TARGETING_LIST = + ImmutableList.of( + ImmutableSet.of(ARMEABI), + ImmutableSet.of(X86_64), + ImmutableSet.of(X86_64, ARMEABI), + ImmutableSet.of(MIPS64), + ImmutableSet.of(MIPS64, ARMEABI), + ImmutableSet.of(MIPS64, X86_64), + ImmutableSet.of(MIPS64, X86_64, ARMEABI)); + + @Test + public void testMultiAbiAliasComparator_exhaustiveTargetingList() { + assertThat( + Comparators.isInStrictOrder( + EXHAUSTIVE_TARGETING_LIST, TargetingComparators.MULTI_ABI_ALIAS_COMPARATOR)) + .isTrue(); + } + + @Test + public void testMultiAbiAliasComparator_realisticTargetingList() { + assertThat( + Comparators.isInStrictOrder( + REALISTIC_TARGETING_LIST, TargetingComparators.MULTI_ABI_ALIAS_COMPARATOR)) + .isTrue(); + } + + @Test + public void testMultiAbiComparator_exhaustiveTargetingList() { + ImmutableList exhaustiveTargetingVariants = + EXHAUSTIVE_TARGETING_LIST.stream() + .map(TargetingComparatorsTest::fromAbiSet) + .collect(toImmutableList()); + + assertThat( + Comparators.isInStrictOrder( + exhaustiveTargetingVariants, TargetingComparators.MULTI_ABI_COMPARATOR)) + .isTrue(); + } + + @Test + public void testMultiAbiComparator_realisticTargetingList() { + ImmutableList realisticTargetingVariants = + REALISTIC_TARGETING_LIST.stream() + .map(TargetingComparatorsTest::fromAbiSet) + .collect(toImmutableList()); + + assertThat( + Comparators.isInStrictOrder( + realisticTargetingVariants, TargetingComparators.MULTI_ABI_COMPARATOR)) + .isTrue(); + } + + private static VariantTargeting fromAbiSet(ImmutableSet abis) { + return variantMultiAbiTargeting(ImmutableSet.of(abis), ImmutableSet.of()); + } +} 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 6f5e961d..e66c26b4 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java @@ -16,6 +16,11 @@ package com.android.tools.build.bundletool.testing; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeVariantTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMultiAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.google.common.collect.ImmutableList.toImmutableList; import com.android.bundle.Commands.ApkDescription; @@ -24,9 +29,12 @@ import com.android.bundle.Commands.ModuleMetadata; import com.android.bundle.Commands.SplitApkMetadata; import com.android.bundle.Commands.StandaloneApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata; +import com.android.bundle.Commands.SystemApkMetadata.SystemApkType; import com.android.bundle.Commands.Variant; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ModuleTargeting; +import com.android.bundle.Targeting.MultiAbiTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.model.ZipPath; @@ -102,6 +110,16 @@ public static Variant standaloneVariant( .build()); } + /** Create standalone variant with multi ABI targeting only. */ + public static Variant multiAbiTargetingStandaloneVariant( + MultiAbiTargeting targeting, ZipPath apkPath) { + return standaloneVariant( + mergeVariantTargeting( + variantSdkTargeting(sdkVersionFrom(1)), variantMultiAbiTargeting(targeting)), + apkMultiAbiTargeting(targeting), + apkPath); + } + public static ApkSet createSplitApkSet(String moduleName, ApkDescription... apkDescription) { return createSplitApkSet( moduleName, @@ -196,4 +214,19 @@ public static ApkSet createStandaloneApkSet(ApkTargeting apkTargeting, Path apkP StandaloneApkMetadata.newBuilder().addFusedModuleName("base"))) .build(); } + + public static ApkSet createSystemApkSet(ApkTargeting apkTargeting, Path apkPath) { + // Note: System APK is represented as a module named "base". + return ApkSet.newBuilder() + .setModuleMetadata(ModuleMetadata.newBuilder().setName("base")) + .addApkDescription( + ApkDescription.newBuilder() + .setPath(apkPath.toString()) + .setTargeting(apkTargeting) + .setSystemApkMetadata( + SystemApkMetadata.newBuilder() + .addFusedModuleName("base") + .setSystemApkType(SystemApkType.SYSTEM))) + .build(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FileUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/FileUtils.java index 75fe6e13..2dcc76a2 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/FileUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FileUtils.java @@ -16,11 +16,19 @@ package com.android.tools.build.bundletool.testing; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; import org.junit.rules.TemporaryFolder; /** File utility functions specific to unit tests. */ @@ -51,6 +59,21 @@ public static Path getRandomFilePath(TemporaryFolder tmp, String prefix, String return tmp.getRoot().toPath().resolve(getRandomFileName(prefix, suffix)); } + public static ImmutableSet getAllFilesInDirectory(Path directory) throws Exception { + try (Stream paths = Files.walk(directory)) { + return paths.filter(Files::isRegularFile).map(Path::getFileName).collect(toImmutableSet()); + } + } + + public static Path uncompressGzipFile(Path gzipPath, Path outputPath) throws Exception { + try (GZIPInputStream gzipInputStream = + new GZIPInputStream(new FileInputStream(gzipPath.toFile())); + FileOutputStream fileOutputStream = new FileOutputStream(outputPath.toFile())) { + ByteStreams.copy(gzipInputStream, fileOutputStream); + } + return outputPath; + } + // Do not instantiate. private FileUtils() {} } 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 916b7489..9417e4d4 100755 --- a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java @@ -21,8 +21,10 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.truth.Truth.assertThat; +import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; +import com.android.bundle.Files.TargetedApexImage; import com.android.bundle.Files.TargetedAssetsDirectory; import com.android.bundle.Files.TargetedNativeDirectory; import com.android.bundle.Targeting.Abi; @@ -58,6 +60,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import com.google.protobuf.Int32Value; import java.util.Arrays; import java.util.Collection; @@ -93,10 +96,7 @@ public static AssetsDirectoryTargeting assetsDirectoryTargeting(AbiTargeting abi } public static AssetsDirectoryTargeting assetsDirectoryTargeting(String architecture) { - AbiAlias alias = - AbiName.fromPlatformName(architecture) - .orElseThrow(() -> new IllegalArgumentException("Unrecognized ABI: " + architecture)) - .toProto(); + AbiAlias alias = toAbiAlias(architecture); return assetsDirectoryTargeting(abiTargeting(alias)); } @@ -135,14 +135,11 @@ public static TargetedNativeDirectory targetedNativeDirectory( // See below, for the targeting dimension helper methods. public static NativeDirectoryTargeting nativeDirectoryTargeting(AbiAlias abi) { - return NativeDirectoryTargeting.newBuilder().setAbi(Abi.newBuilder().setAlias(abi)).build(); + return NativeDirectoryTargeting.newBuilder().setAbi(toAbi(abi)).build(); } public static NativeDirectoryTargeting nativeDirectoryTargeting(String architecture) { - AbiAlias alias = - AbiName.fromPlatformName(architecture) - .orElseThrow(() -> new IllegalArgumentException("Unrecognized ABI: " + architecture)) - .toProto(); + AbiAlias alias = toAbiAlias(architecture); return nativeDirectoryTargeting(alias); } @@ -155,11 +152,22 @@ public static NativeDirectoryTargeting nativeDirectoryTargeting( // Apex image file targeting helpers. - public static ApexImageTargeting apexImageSingleAbiTargeting(AbiAlias abi) { + /** Builds APEX images proto from a collection of targeted images. */ + public static ApexImages apexImages(TargetedApexImage... targetedApexImages) { + return ApexImages.newBuilder().addAllImage(Lists.newArrayList(targetedApexImages)).build(); + } + + /** Builds APEX targeted image from the image file path and its targeting. */ + public static TargetedApexImage targetedApexImage(String path, ApexImageTargeting targeting) { + return TargetedApexImage.newBuilder().setPath(path).setTargeting(targeting).build(); + } + + /** Builds APEX image targeting (no alternatives) according to the Abi names. */ + public static ApexImageTargeting apexImageTargeting(String... architectures) { + MultiAbi.Builder multiAbi = MultiAbi.newBuilder(); + Arrays.stream(architectures).forEach(abi -> multiAbi.addAbi(toAbi(abi))); return ApexImageTargeting.newBuilder() - .setMultiAbi( - MultiAbiTargeting.newBuilder() - .addValue(MultiAbi.newBuilder().addAbi(Abi.newBuilder().setAlias(abi)))) + .setMultiAbi(MultiAbiTargeting.newBuilder().addValue(multiAbi)) .build(); } @@ -188,6 +196,58 @@ public static ApkTargeting apkAbiTargeting(AbiAlias abiAlias) { return apkAbiTargeting(abiTargeting(abiAlias)); } + /** Builds APK targeting, of multi-Abi targeting only. */ + public static ApkTargeting apkMultiAbiTargeting(MultiAbiTargeting multiAbiTargeting) { + return ApkTargeting.newBuilder().setMultiAbiTargeting(multiAbiTargeting).build(); + } + + /** Builds APK targeting, of multi-Abi targeting of a single architecture. */ + public static ApkTargeting apkMultiAbiTargeting(AbiAlias abiAlias) { + return apkMultiAbiTargeting(multiAbiTargeting(abiAlias)); + } + + /** + * Builds APK targeting of a single architecture, with multi-Abi alternatives of single + * architecture each. + * + * @param abiAlias single Abi to target by. + * @param alternativeAbis a set of Abis, each one mapped to a single-Abi alternative (rather than + * one targeting of multiple Abis). + */ + public static ApkTargeting apkMultiAbiTargeting( + AbiAlias abiAlias, ImmutableSet alternativeAbis) { + return apkMultiAbiTargeting(multiAbiTargeting(abiAlias, alternativeAbis)); + } + + /** + * Builds APK multi-Abi targeting of arbitrary values and alternatives. + * + * @param abiAliases a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated MultiAbiTargeting.value. + * @param alternatives a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated + * MultiAbiTargeting.alternatives. + */ + public static ApkTargeting apkMultiAbiTargeting( + ImmutableSet> abiAliases, + ImmutableSet> alternatives) { + return apkMultiAbiTargeting(multiAbiTargeting(abiAliases, alternatives)); + } + + /** + * Builds APK multi-Abi targeting of single (multi Abi) values and arbitrary alternatives. + * + * @param targeting a set of Abi aliases, corresponding to a single value of MultiAbiTargeting. + * @param allTargeting a set of all expected 'targeting' sets. The alternatives are built from + * this set minus the 'targeting' set. + */ + public static ApkTargeting apkMultiAbiTargetingFromAllTargeting( + ImmutableSet targeting, ImmutableSet> allTargeting) { + return apkMultiAbiTargeting( + ImmutableSet.of(targeting), + Sets.difference(allTargeting, ImmutableSet.of(targeting)).immutableCopy()); + } + public static ApkTargeting apkDensityTargeting(ScreenDensityTargeting screenDensityTargeting) { return ApkTargeting.newBuilder().setScreenDensityTargeting(screenDensityTargeting).build(); } @@ -261,10 +321,70 @@ public static VariantTargeting variantAbiTargeting(Abi value, ImmutableSet .build(); } + /** Builds variant targeting, of multi-Abi targeting only. */ + public static VariantTargeting variantMultiAbiTargeting(MultiAbiTargeting multiAbiTargeting) { + return VariantTargeting.newBuilder().setMultiAbiTargeting(multiAbiTargeting).build(); + } + + /** + * Builds variant multi-Abi targeting of a single architecture, with multi-Abi alternatives of + * single architecture each. + * + * @param value single Abi to target by. + * @param alternatives a set of Abis, each one mapped to a single-Abi alternative (rather than one + * targeting of multiple Abis). + */ + public static VariantTargeting variantMultiAbiTargeting( + AbiAlias value, ImmutableSet alternatives) { + return variantMultiAbiTargeting(multiAbiTargeting(value, alternatives)); + } + + /** + * Builds variant multi-Abi targeting of arbitrary values and alternatives. + * + * @param abiAliases a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated MultiAbiTargeting.value. + * @param alternatives a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated + * MultiAbiTargeting.alternatives. + */ + public static VariantTargeting variantMultiAbiTargeting( + ImmutableSet> abiAliases, + ImmutableSet> alternatives) { + return variantMultiAbiTargeting(multiAbiTargeting(abiAliases, alternatives)); + } + + /** + * Builds variant multi-Abi targeting of single (multi Abi) values and arbitrary alternatives. + * + * @param targeting a set of Abi aliases, corresponding to a single value of MultiAbiTargeting. + * @param allTargeting a set of all expected 'targeting' sets. The alternatives are built from + * this set minus the 'targeting' set. + */ + public static VariantTargeting variantMultiAbiTargetingFromAllTargeting( + ImmutableSet targeting, ImmutableSet> allTargeting) { + return variantMultiAbiTargeting( + ImmutableSet.of(targeting), + Sets.difference(allTargeting, ImmutableSet.of(targeting)).immutableCopy()); + } + + /** Builds Abi proto from its alias. */ public static Abi toAbi(AbiAlias alias) { return Abi.newBuilder().setAlias(alias).build(); } + /** Builds Abi proto from the alias's name, given as a String. */ + public static Abi toAbi(String abi) { + return toAbi(toAbiAlias(abi)); + } + + /** Builds AbiAlias proto from the alias's name, given as a String. */ + static AbiAlias toAbiAlias(String abi) { + return AbiName.fromPlatformName(abi) + .orElseThrow(() -> new IllegalArgumentException("Unrecognized ABI: " + abi)) + .toProto(); + } + public static VariantTargeting variantMinSdkTargeting( int minSdkVersion, int... alternativeSdkVersions) { @@ -376,21 +496,74 @@ public static AbiTargeting abiTargeting(AbiAlias abi, ImmutableSet alt public static AbiTargeting abiTargeting( ImmutableSet abiAliases, ImmutableSet alternatives) { return AbiTargeting.newBuilder() - .addAllValue( - abiAliases - .stream() - .map(alias -> Abi.newBuilder().setAlias(alias).build()) - .collect(toImmutableList())) + .addAllValue(abiAliases.stream().map(TargetingUtils::toAbi).collect(toImmutableList())) .addAllAlternatives( - alternatives - .stream() - .map(alias -> Abi.newBuilder().setAlias(alias).build()) - .collect(toImmutableList())) + alternatives.stream().map(TargetingUtils::toAbi).collect(toImmutableList())) + .build(); + } + + // Multi ABI targeting + + /** Builds multi-Abi targeting of a single value, of one architecture (no alternatives). */ + public static MultiAbiTargeting multiAbiTargeting(AbiAlias abi) { + return multiAbiTargeting(ImmutableSet.of(ImmutableSet.of(abi)), ImmutableSet.of()); + } + + /** + * Builds multi-Abi targeting of arbitrary values with no alternatives. + * + * @param abiAliases a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated MultiAbiTargeting.value. + */ + public static MultiAbiTargeting multiAbiTargeting( + ImmutableSet> abiAliases) { + return MultiAbiTargeting.newBuilder().addAllValue(buildMultiAbis(abiAliases)).build(); + } + + /** + * Builds multi-Abi targeting of a single architecture, with multi-Abi alternatives of single + * architecture each. + * + * @param abi single Abi to target by. + * @param alternatives a set of Abis, each one mapped to a single-Abi alternative (rather than one + * targeting of multiple Abis). + */ + public static MultiAbiTargeting multiAbiTargeting( + AbiAlias abi, ImmutableSet alternatives) { + // Each element in 'alternatives' represent an alternative, not a MultiAbi. + ImmutableSet> alternativeSet = + alternatives.stream().map(ImmutableSet::of).collect(toImmutableSet()); + return multiAbiTargeting(ImmutableSet.of(ImmutableSet.of(abi)), alternativeSet); + } + + /** + * Builds multi-Abi targeting of arbitrary values and alternatives. + * + * @param abiAliases a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated MultiAbiTargeting.value. + * @param alternatives a set of sets of Abi aliases. Each inner set is converted to the repeated + * MultiAbi.abi, and the outer set is converted to the repeated + * MultiAbiTargeting.alternatives. + */ + public static MultiAbiTargeting multiAbiTargeting( + ImmutableSet> abiAliases, + ImmutableSet> alternatives) { + return MultiAbiTargeting.newBuilder() + .addAllValue(buildMultiAbis(abiAliases)) + .addAllAlternatives(buildMultiAbis(alternatives)) .build(); } - public static Abi abi(AbiAlias abiAlias) { - return Abi.newBuilder().setAlias(abiAlias).build(); + private static ImmutableList buildMultiAbis( + ImmutableSet> abiAliases) { + return abiAliases.stream() + .map( + aliases -> + MultiAbi.newBuilder() + .addAllAbi( + aliases.stream().map(TargetingUtils::toAbi).collect(toImmutableList())) + .build()) + .collect(toImmutableList()); } // Graphics API Targeting diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtilsTest.java new file mode 100755 index 00000000..85340806 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtilsTest.java @@ -0,0 +1,265 @@ +/* + * 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.testing; + +import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; +import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86; +import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Files.ApexImages; +import com.android.bundle.Files.TargetedApexImage; +import com.android.bundle.Targeting.Abi; +import com.android.bundle.Targeting.Abi.AbiAlias; +import com.android.bundle.Targeting.ApexImageTargeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.MultiAbi; +import com.android.bundle.Targeting.MultiAbiTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TargetingUtilsTest { + + private static final MultiAbiTargeting SINGLE_ABI_NO_ALTERNATIVES = + MultiAbiTargeting.newBuilder().addValue(multiAbi(X86)).build(); + private static final MultiAbiTargeting SINGLE_ABI_WITH_ALTERNATIVES = + MultiAbiTargeting.newBuilder() + .addValue(multiAbi(X86)) + .addAlternatives(multiAbi(ARMEABI_V7A)) + .addAlternatives(multiAbi(ARM64_V8A)) + .build(); + private static final MultiAbiTargeting MULTI_ABI_NO_ALTERNATIVES = + MultiAbiTargeting.newBuilder().addValue(multiAbi(ARMEABI_V7A, ARM64_V8A)).build(); + private static final MultiAbiTargeting MULTI_ABI_WITH_ALTERNATIVES = + MultiAbiTargeting.newBuilder() + .addValue(multiAbi(X86)) + .addValue(multiAbi(ARMEABI_V7A, ARM64_V8A)) + .addAlternatives(multiAbi(X86_64)) + .build(); + + @Test + public void toAbiAlias_validAbi_succeeds() { + assertThat(TargetingUtils.toAbiAlias("x86")).isEqualTo(X86); + } + + @Test + public void toAbiAlias_unknownAbi_throws() { + Exception e = + assertThrows(IllegalArgumentException.class, () -> TargetingUtils.toAbiAlias("sparc")); + + assertThat(e).hasMessageThat().contains("Unrecognized ABI"); + } + + @Test + public void toAbi_validAbi_succeeds() { + Abi expectedAbi = Abi.newBuilder().setAlias(X86).build(); + + assertThat(TargetingUtils.toAbi("x86")).isEqualTo(expectedAbi); + } + + @Test + public void toAbi_unknownAbi_throws() { + Exception e = assertThrows(IllegalArgumentException.class, () -> TargetingUtils.toAbi("sparc")); + + assertThat(e).hasMessageThat().contains("Unrecognized ABI"); + } + + @Test + public void multiAbiTargeting_singleAbiNoAlternatives() { + assertThat(TargetingUtils.multiAbiTargeting(X86)).isEqualTo(SINGLE_ABI_NO_ALTERNATIVES); + } + + @Test + public void multiAbiTargeting_singleAbiAndAlternatives() { + assertThat(TargetingUtils.multiAbiTargeting(X86, ImmutableSet.of(ARMEABI_V7A, ARM64_V8A))) + .ignoringRepeatedFieldOrder() + .isEqualTo(SINGLE_ABI_WITH_ALTERNATIVES); + } + + @Test + public void multiAbiTargeting_multipleAbisNoAlternatives() { + ImmutableSet> abiAliases = + ImmutableSet.of(ImmutableSet.of(ARMEABI_V7A, ARM64_V8A)); + + assertThat(TargetingUtils.multiAbiTargeting(abiAliases)) + .ignoringRepeatedFieldOrder() + .isEqualTo(MULTI_ABI_NO_ALTERNATIVES); + } + + @Test + public void multiAbiTargeting_multipleAbisAndAlternatives() { + ImmutableSet> abiAliases = + ImmutableSet.of(ImmutableSet.of(X86), ImmutableSet.of(ARMEABI_V7A, ARM64_V8A)); + ImmutableSet> alternatives = ImmutableSet.of(ImmutableSet.of(X86_64)); + + assertThat(TargetingUtils.multiAbiTargeting(abiAliases, alternatives)) + .ignoringRepeatedFieldOrder() + .isEqualTo(MULTI_ABI_WITH_ALTERNATIVES); + } + + @Test + public void apexImages() { + TargetedApexImage firstTargeting = TargetedApexImage.newBuilder().setPath("path1").build(); + TargetedApexImage secondTargeting = TargetedApexImage.newBuilder().setPath("path2").build(); + + ApexImages apexImages = TargetingUtils.apexImages(firstTargeting, secondTargeting); + + assertThat(apexImages.getImageList()).containsExactly(firstTargeting, secondTargeting); + } + + @Test + public void targetedApexImage() { + ApexImageTargeting targeting = + ApexImageTargeting.newBuilder().setMultiAbi(MULTI_ABI_WITH_ALTERNATIVES).build(); + + TargetedApexImage apexImage = TargetingUtils.targetedApexImage("path", targeting); + + assertThat(apexImage.getPath()).isEqualTo("path"); + assertThat(apexImage.getTargeting()).ignoringRepeatedFieldOrder().isEqualTo(targeting); + } + + @Test + public void apexImageTargeting() { + ApexImageTargeting expected = + ApexImageTargeting.newBuilder().setMultiAbi(MULTI_ABI_NO_ALTERNATIVES).build(); + + assertThat(TargetingUtils.apexImageTargeting("armeabi-v7a", "arm64-v8a")).isEqualTo(expected); + } + + @Test + public void apkMultiAbiTargeting_byAbiAlias() { + ApkTargeting expectedTargeting = + ApkTargeting.newBuilder().setMultiAbiTargeting(SINGLE_ABI_NO_ALTERNATIVES).build(); + + assertThat(TargetingUtils.apkMultiAbiTargeting(X86)).isEqualTo(expectedTargeting); + } + + @Test + public void apkMultiAbiTargeting_byMultiAbiTargeting() { + ApkTargeting expectedTargeting = + ApkTargeting.newBuilder().setMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES).build(); + + assertThat(TargetingUtils.apkMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES)) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void apkMultiAbiTargeting_byAbiAliasAndAlternativesSet() { + ApkTargeting expectedTargeting = + ApkTargeting.newBuilder().setMultiAbiTargeting(SINGLE_ABI_WITH_ALTERNATIVES).build(); + + assertThat( + TargetingUtils.apkMultiAbiTargeting( + AbiAlias.X86, ImmutableSet.of(ARMEABI_V7A, ARM64_V8A))) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void apkMultiAbiTargeting_byMultipleAbisAndAlternatives() { + ApkTargeting expectedTargeting = + ApkTargeting.newBuilder().setMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES).build(); + + assertThat( + TargetingUtils.apkMultiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(X86), ImmutableSet.of(ARMEABI_V7A, ARM64_V8A)), + ImmutableSet.of(ImmutableSet.of(X86_64)))) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void apkMultiAbiTargetingFromAllTergeting() { + ImmutableSet> allTargeting = + ImmutableSet.of( + ImmutableSet.of(ARMEABI_V7A), ImmutableSet.of(ARM64_V8A), ImmutableSet.of(X86)); + + ApkTargeting expectedTargeting = + ApkTargeting.newBuilder().setMultiAbiTargeting(SINGLE_ABI_WITH_ALTERNATIVES).build(); + assertThat( + TargetingUtils.apkMultiAbiTargetingFromAllTargeting(ImmutableSet.of(X86), allTargeting)) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void variantMultiAbiTargeting_byMultiAbiTargeting() { + VariantTargeting expectedTargeting = + VariantTargeting.newBuilder().setMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES).build(); + + assertThat(TargetingUtils.variantMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES)) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void variantMultiAbiTargeting_byAbiAliasAndAlternativesSet() { + VariantTargeting expectedTargeting = + VariantTargeting.newBuilder().setMultiAbiTargeting(SINGLE_ABI_WITH_ALTERNATIVES).build(); + + assertThat( + TargetingUtils.variantMultiAbiTargeting(X86, ImmutableSet.of(ARMEABI_V7A, ARM64_V8A))) + .isEqualTo(expectedTargeting); + } + + @Test + public void variantMultiAbiTargeting_byMultipleAbisAndAlternatives() { + VariantTargeting expectedTargeting = + VariantTargeting.newBuilder().setMultiAbiTargeting(MULTI_ABI_WITH_ALTERNATIVES).build(); + + assertThat( + TargetingUtils.variantMultiAbiTargeting( + ImmutableSet.of(ImmutableSet.of(X86), ImmutableSet.of(ARMEABI_V7A, ARM64_V8A)), + ImmutableSet.of(ImmutableSet.of(X86_64)))) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + @Test + public void variantMultiAbiTargetingFromAllTergeting() { + ImmutableSet> allTargeting = + ImmutableSet.of( + ImmutableSet.of(ARMEABI_V7A), ImmutableSet.of(ARM64_V8A), ImmutableSet.of(X86)); + + VariantTargeting expectedTargeting = + VariantTargeting.newBuilder().setMultiAbiTargeting(SINGLE_ABI_WITH_ALTERNATIVES).build(); + assertThat( + TargetingUtils.variantMultiAbiTargetingFromAllTargeting( + ImmutableSet.of(X86), allTargeting)) + .ignoringRepeatedFieldOrder() + .isEqualTo(expectedTargeting); + } + + private static MultiAbi multiAbi(AbiAlias... aliases) { + return MultiAbi.newBuilder() + .addAllAbi( + Arrays.stream(aliases) + .map(alias -> Abi.newBuilder().setAlias(alias).build()) + .collect(toImmutableList())) + .build(); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/utils/ResultUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/utils/ResultUtilsTest.java index 90328e7a..2cce29f2 100755 --- a/src/test/java/com/android/tools/build/bundletool/utils/ResultUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/utils/ResultUtilsTest.java @@ -23,6 +23,7 @@ import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSplitApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createStandaloneApkSet; +import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSystemApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; @@ -162,6 +163,14 @@ public void filterStandaloneApkVariant() throws Exception { assertThat(ResultUtils.standaloneApkVariants(apksResult)).containsExactly(standaloneVariant); } + @Test + public void filterSystemApkVariant() throws Exception { + Variant systemVariant = createSystemVariant(); + BuildApksResult apksResult = BuildApksResult.newBuilder().addVariant(systemVariant).build(); + + assertThat(ResultUtils.systemApkVariants(apksResult)).containsExactly(systemVariant); + } + @Test public void isInstantApkVariantTrue() throws Exception { Variant variant = createInstantVariant(); @@ -169,6 +178,7 @@ public void isInstantApkVariantTrue() throws Exception { assertThat(ResultUtils.isInstantApkVariant(variant)).isTrue(); assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); } @Test @@ -178,6 +188,7 @@ public void isStandaloneApkVariantTrue() throws Exception { assertThat(ResultUtils.isStandaloneApkVariant(variant)).isTrue(); assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); } @Test @@ -187,6 +198,17 @@ public void isSplitApkVariantTrue() throws Exception { assertThat(ResultUtils.isSplitApkVariant(variant)).isTrue(); assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isSystemApkVariant(variant)).isFalse(); + } + + @Test + public void isSystemApkVariantTrue() throws Exception { + Variant variant = createSystemVariant(); + + assertThat(ResultUtils.isSplitApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isStandaloneApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isInstantApkVariant(variant)).isFalse(); + assertThat(ResultUtils.isSystemApkVariant(variant)).isTrue(); } private Variant createInstantVariant() { @@ -217,7 +239,14 @@ private Variant createSplitVariant() { private Variant createStandaloneVariant() { Path apkPreL = ZipPath.create("apkPreL.apk"); return createVariant( - variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(SdkVersion.getDefaultInstance())), createStandaloneApkSet(ApkTargeting.getDefaultInstance(), apkPreL)); } + + private Variant createSystemVariant() { + Path systemApk = ZipPath.create("system.apk"); + return createVariant( + variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSystemApkSet(ApkTargeting.getDefaultInstance(), systemApk)); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/utils/SplitsXmlInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/utils/SplitsXmlInjectorTest.java index 92bf7444..e5616856 100755 --- a/src/test/java/com/android/tools/build/bundletool/utils/SplitsXmlInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/utils/SplitsXmlInjectorTest.java @@ -44,13 +44,17 @@ import com.android.tools.build.bundletool.testing.ResourceTableBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Before; import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(Theories.class) public class SplitsXmlInjectorTest { private static final String PACKAGE_NAME = "com.example.app"; @@ -164,14 +168,20 @@ public void process() throws Exception { .isEqualTo(expectedSplitsProtoXml); } + @DataPoints("standaloneSplitTypes") + public static final ImmutableSet STANDALONE_SPLIT_TYPES = + ImmutableSet.of(SplitType.STANDALONE, SplitType.SYSTEM); + @Test - public void process_standalone() throws Exception { + @Theory + public void process_standaloneSplitTypes( + @FromDataPoints("standaloneSplitTypes") SplitType standaloneSplitType) throws Exception { ModuleSplit standalone = createModuleSplit( BASE_MODULE_NAME, /* splitId= */ "", /* masterSplit= */ true, - STANDALONE, + standaloneSplitType, /* languageTargeting= */ null); ResourceTable standaloneResourceTable = new ResourceTableBuilder() diff --git a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java index 5bd9ba54..f77bcded 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java @@ -37,6 +37,7 @@ import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MaxSdkLessThanMinInstantSdk; import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MinSdkGreaterThanMaxSdkException; import com.android.tools.build.bundletool.exceptions.manifest.ManifestSdkTargetingException.MinSdkInvalidException; +import com.android.tools.build.bundletool.exceptions.manifest.ManifestVersionCodeConflictException; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ManifestProtoUtils.ManifestMutator; @@ -656,9 +657,9 @@ public void bundleModules_differentVersionCode_throws() throws Exception { .setManifest(androidManifest("com.test", withVersionCode(3))) .build()); - Throwable exception = + ManifestVersionCodeConflictException exception = assertThrows( - ValidationException.class, + ManifestVersionCodeConflictException.class, () -> new AndroidManifestValidator().validateAllModules(bundleModules)); assertThat(exception) .hasMessageThat() diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java index 344c8394..29164f16 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ApexBundleValidatorTest.java @@ -36,7 +36,10 @@ public class ApexBundleValidatorTest { private static final String PKG_NAME = "com.test.app"; private static final ApexImages APEX_CONFIG = ApexImages.newBuilder() + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.img")) .addImage(TargetedApexImage.newBuilder().setPath("apex/x86.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/armeabi-v7a.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/arm64-v8a.img")) .build(); @Test @@ -52,7 +55,7 @@ public void validateModule_unexpectedFile_throws() throws Exception { new BundleModuleBuilder("apexTestModule") .setManifest(androidManifest(PKG_NAME)) .setApexConfig(APEX_CONFIG) - .addFile("root/manifest.json") + .addFile("root/apex_manifest.json") .addFile("apex/x86.img") .addFile("root/unexpected.txt") .build(); @@ -86,9 +89,9 @@ public void validateModule_untargetedImageFile_throws() throws Exception { new BundleModuleBuilder("apexTestModule") .setManifest(androidManifest(PKG_NAME)) .setApexConfig(APEX_CONFIG) - .addFile("root/manifest.json") + .addFile("root/apex_manifest.json") .addFile("apex/x86.img") - .addFile("apex/x86_64.img") + .addFile("apex/x86_64.x86.img") .build(); ValidationException exception = @@ -104,7 +107,7 @@ public void validateModule_missingTargetedImageFile_throws() throws Exception { new BundleModuleBuilder("apexTestModule") .setManifest(androidManifest(PKG_NAME)) .setApexConfig(APEX_CONFIG) - .addFile("root/manifest.json") + .addFile("root/apex_manifest.json") // No image files under apex/. .build(); @@ -115,6 +118,65 @@ public void validateModule_missingTargetedImageFile_throws() throws Exception { assertThat(exception).hasMessageThat().contains("Targeted APEX image files are missing"); } + @Test + public void validateModule_imageFilesTargetSameSetOfAbis_throws() throws Exception { + ApexImages apexConfig = + ApexImages.newBuilder() + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.x86.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86.armeabi-v7a.x86_64.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.x86.armeabi-v7a.img")) + .build(); + BundleModule apexModule = + new BundleModuleBuilder("apexTestModule") + .setManifest(androidManifest(PKG_NAME)) + .setApexConfig(apexConfig) + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.x86.img") + .addFile("apex/x86.armeabi-v7a.x86_64.img") + .addFile("apex/x86_64.x86.armeabi-v7a.img") + .build(); + + ValidationException exception = + assertThrows( + ValidationException.class, () -> new ApexBundleValidator().validateModule(apexModule)); + + assertThat(exception) + .hasMessageThat() + .contains("Every APEX image file must target a unique set of architectures"); + } + + @Test + public void validateModule_singletonAbiMissing_throws() throws Exception { + ApexImages apexConfig = + ApexImages.newBuilder() + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/armeabi-v7a.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.x86.img")) + .addImage(TargetedApexImage.newBuilder().setPath("apex/x86_64.armeabi-v7a.img")) + .build(); + BundleModule apexModule = + new BundleModuleBuilder("apexTestModule") + .setManifest(androidManifest(PKG_NAME)) + .setApexConfig(apexConfig) + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.img") + .addFile("apex/x86.img") + .addFile("apex/armeabi-v7a.img") + // arm64-v8a.img missing. + .addFile("apex/x86_64.x86.img") + .addFile("apex/x86_64.armeabi-v7a.img") + .build(); + + ValidationException exception = + assertThrows( + ValidationException.class, () -> new ApexBundleValidator().validateModule(apexModule)); + + assertThat(exception) + .hasMessageThat() + .contains("APEX bundle must contain all these singleton architectures"); + } + @Test public void validateAllModules_singleApexModule_succeeds() throws Exception { BundleModule apexModule = validApexModule(); @@ -156,8 +218,11 @@ private BundleModule validApexModule() throws IOException { return new BundleModuleBuilder("apexTestModule") .setManifest(androidManifest(PKG_NAME)) .setApexConfig(APEX_CONFIG) - .addFile("root/manifest.json") + .addFile("root/apex_manifest.json") + .addFile("apex/x86_64.img") .addFile("apex/x86.img") + .addFile("apex/armeabi-v7a.img") + .addFile("apex/arm64-v8a.img") .build(); } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java index 1d602681..fd9fb45c 100755 --- a/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/BundleFilesValidatorTest.java @@ -314,6 +314,18 @@ public void validateApexFile_unknownMultipleAbi_throws() throws Exception { assertThat(e).hasMessageThat().contains("Unrecognized native architecture for file"); } + @Test + public void validateApexFile_repeatingAbis_throws() throws Exception { + ZipPath repeatingAbisFile = ZipPath.create("apex/x86.x86_64.x86.img"); + + ValidationException e = + assertThrows( + ValidationException.class, + () -> new BundleFilesValidator().validateModuleFile(repeatingAbisFile)); + + assertThat(e).hasMessageThat().contains("Repeating architectures in APEX system image file"); + } + @Test public void validateOtherFile_inModuleRoot_throws() throws Exception { ZipPath otherFile = ZipPath.create("in-root.txt");