diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/Custom.md b/.github/ISSUE_TEMPLATE/Custom.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 6f171528..b6c20197 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [0.12.0](https://github.com/google/bundletool/releases) +Latest release: [0.13.0](https://github.com/google/bundletool/releases) diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 diff --git a/gradle.properties b/gradle.properties old mode 100644 new mode 100755 index a5ec9887..21cd10ef --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 0.12.0 +release_version = 0.13.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 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 fb01a105..15727ff7 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -38,6 +38,7 @@ import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.Password; import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.SourceStamp; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; @@ -105,7 +106,9 @@ public final boolean isAnySystemMode() { Flag.enumSet("optimize-for", OptimizationDimension.class); private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); 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 BUILD_MODE_FLAG = + Flag.enumFlag("mode", ApkBuildMode.class); + private static final Flag LOCAL_TESTING_MODE_FLAG = Flag.booleanFlag("local-testing"); private static final Flag ADB_PATH_FLAG = Flag.path("adb"); private static final Flag CONNECTED_DEVICE_FLAG = Flag.booleanFlag("connected-device"); @@ -118,8 +121,9 @@ public final boolean isAnySystemMode() { // Signing-related flags: should match flags from apksig library. private static final Flag KEYSTORE_FLAG = Flag.path("ks"); private static final Flag KEY_ALIAS_FLAG = Flag.string("ks-key-alias"); - private static final Flag KEYSTORE_PASSWORD = Flag.password("ks-pass"); - private static final Flag KEY_PASSWORD = Flag.password("key-pass"); + private static final Flag KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass"); + private static final Flag KEY_PASSWORD_FLAG = Flag.password("key-pass"); + private static final String APK_SET_ARCHIVE_EXTENSION = "apks"; @@ -150,6 +154,8 @@ public final boolean isAnySystemMode() { public abstract ApkBuildMode getApkBuildMode(); + public abstract boolean getLocalTestingMode(); + public abstract Optional getAapt2Command(); public abstract Optional getSigningConfiguration(); @@ -175,10 +181,12 @@ ListeningExecutorService getExecutorService() { public abstract Optional getOutputPrintStream(); + public static Builder builder() { return new AutoValue_BuildApksCommand.Builder() .setOverwriteOutput(false) .setApkBuildMode(DEFAULT) + .setLocalTestingMode(false) .setGenerateOnlyForConnectedDevice(false) .setCreateApkSetArchive(true) .setOptimizationDimensions(ImmutableSet.of()) @@ -215,6 +223,13 @@ public abstract Builder setOptimizationDimensions( */ public abstract Builder setApkBuildMode(ApkBuildMode mode); + /** + * Sets whether the APKs should be built in local testing mode. + * + *

The default is {@code false}. + */ + public abstract Builder setLocalTestingMode(boolean enableLocalTesting); + /** * Sets if the generated APK Set will contain APKs compatible only with the connected device. */ @@ -316,6 +331,7 @@ public Builder setExecutorService(ListeningExecutorService executorService) { /** 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() { @@ -436,7 +452,8 @@ static BuildApksCommand fromFlags( aapt2Path -> buildApksCommand.setAapt2Command(Aapt2Command.createFromExecutablePath(aapt2Path))); - MODE_FLAG.getValue(flags).ifPresent(buildApksCommand::setApkBuildMode); + BUILD_MODE_FLAG.getValue(flags).ifPresent(buildApksCommand::setApkBuildMode); + LOCAL_TESTING_MODE_FLAG.getValue(flags).ifPresent(buildApksCommand::setLocalTestingMode); MAX_THREADS_FLAG .getValue(flags) .ifPresent( @@ -449,8 +466,8 @@ static BuildApksCommand fromFlags( // Signing-related arguments. Optional keystorePath = KEYSTORE_FLAG.getValue(flags); Optional keyAlias = KEY_ALIAS_FLAG.getValue(flags); - Optional keystorePassword = KEYSTORE_PASSWORD.getValue(flags); - Optional keyPassword = KEY_PASSWORD.getValue(flags); + Optional keystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags); + Optional keyPassword = KEY_PASSWORD_FLAG.getValue(flags); if (keystorePath.isPresent() && keyAlias.isPresent()) { buildApksCommand.setSigningConfiguration( @@ -505,7 +522,7 @@ static BuildApksCommand fromFlags( buildApksCommand.setAdbPath(adbPath).setAdbServer(adbServer); } - ApkBuildMode apkBuildMode = MODE_FLAG.getValue(flags).orElse(DEFAULT); + ApkBuildMode apkBuildMode = BUILD_MODE_FLAG.getValue(flags).orElse(DEFAULT); boolean supportsPartialDeviceSpecs = apkBuildMode.isAnySystemMode(); Function deviceSpecParser = @@ -592,7 +609,7 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(MODE_FLAG.getName()) + .setFlagName(BUILD_MODE_FLAG.getName()) .setExampleValue(joinFlagOptions(ApkBuildMode.values())) .setOptional(true) .setDescription( @@ -628,7 +645,7 @@ public static CommandHelp help() { + "Acceptable values are '%s'. This flag should be only be set with " + "--%s=%s flag.", joinFlagOptions(OptimizationDimension.values()), - MODE_FLAG.getName(), + BUILD_MODE_FLAG.getName(), DEFAULT.getLowerCaseName()) .build()) .addFlag( @@ -652,7 +669,7 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(KEYSTORE_PASSWORD.getName()) + .setFlagName(KEYSTORE_PASSWORD_FLAG.getName()) .setExampleValue("[pass|file]:value") .setOptional(true) .setDescription( @@ -664,7 +681,7 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(KEY_PASSWORD.getName()) + .setFlagName(KEY_PASSWORD_FLAG.getName()) .setExampleValue("key-password") .setOptional(true) .setDescription( @@ -683,7 +700,7 @@ public static CommandHelp help() { "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. This flag should be only be set with --%s=%s flag.", - MODE_FLAG.getName(), DEFAULT.getLowerCaseName()) + BUILD_MODE_FLAG.getName(), DEFAULT.getLowerCaseName()) .build()) .addFlag( FlagDescription.builder() @@ -716,7 +733,7 @@ public static CommandHelp help() { + "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(), + BUILD_MODE_FLAG.getName(), DEFAULT.getLowerCaseName()) .build()) .addFlag( @@ -731,6 +748,18 @@ public static CommandHelp help() { SYSTEM.getLowerCaseName(), SYSTEM_COMPRESSED.getLowerCaseName()) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(LOCAL_TESTING_MODE_FLAG.getName()) + .setOptional(true) + .setDescription( + "If enabled, the APK set will be built in local testing mode, which includes" + + " additional metadata in the output. When `bundletool %s` is later used" + + " to install APKs from this set on a device, it will additionally push" + + " all dynamic module splits and asset packs to a location that can be" + + " accessed by the Play Core API.", + InstallApksCommand.COMMAND_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 ad203dfb..a2410f37 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -25,6 +25,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Config.BundleConfig; import com.android.bundle.Config.Compression; import com.android.bundle.Config.SuffixStripping; @@ -62,6 +63,8 @@ import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.optimizations.OptimizationsMerger; import com.android.tools.build.bundletool.preprocessors.AppBundle64BitNativeLibrariesPreprocessor; +import com.android.tools.build.bundletool.preprocessors.EntryCompressionPreprocessor; +import com.android.tools.build.bundletool.preprocessors.LocalTestingPreprocessor; import com.android.tools.build.bundletool.splitters.ApkGenerationConfiguration; import com.android.tools.build.bundletool.splitters.AssetSlicesGenerator; import com.android.tools.build.bundletool.splitters.ResourceAnalyzer; @@ -79,6 +82,7 @@ import java.util.logging.Logger; import java.util.stream.Stream; import java.util.zip.ZipFile; +import javax.annotation.CheckReturnValue; /** Executes the "build-apks" command. */ final class BuildApksManager { @@ -137,9 +141,7 @@ private void executeWithZip(ZipFile bundleZip, Optional deviceSpec) AppBundle appBundle = AppBundle.buildFromZip(bundleZip); bundleValidator.validate(appBundle); - appBundle = - new AppBundle64BitNativeLibrariesPreprocessor(command.getOutputPrintStream()) - .preprocess(appBundle); + appBundle = applyPreprocessors(appBundle); ImmutableSet requestedModules = command.getModules().isEmpty() @@ -230,8 +232,13 @@ private void executeWithZip(ZipFile bundleZip, Optional deviceSpec) command.getApkListener().orElse(ApkListener.NO_OP), command.getApkModifier().orElse(ApkModifier.NO_OP), command.getFirstVariantNumber().orElse(0)); + apkSerializerManager.populateApkSetBuilder( - generatedApks, generatedAssetSlices.build(), command.getApkBuildMode(), deviceSpec); + generatedApks, + generatedAssetSlices.build(), + command.getApkBuildMode(), + deviceSpec, + getLocalTestingInfo(appBundle)); if (command.getOverwriteOutput()) { Files.deleteIfExists(command.getOutputFile()); @@ -382,7 +389,10 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat installLocation.equals("auto") || installLocation.equals("preferExternal")) .orElse(false)); - apkGenerationConfiguration.setMasterPinnedResources(appBundle.getMasterPinnedResources()); + apkGenerationConfiguration.setMasterPinnedResourceIds(appBundle.getMasterPinnedResourceIds()); + + apkGenerationConfiguration.setMasterPinnedResourceNames( + appBundle.getMasterPinnedResourceNames()); apkGenerationConfiguration.setSuffixStrippings(getSuffixStrippings(bundleConfig)); @@ -471,6 +481,30 @@ private static boolean shouldStrip64BitLibrariesFromShards(AppBundle appBundle) .getStrip64BitLibraries(); } + @CheckReturnValue + private AppBundle applyPreprocessors(AppBundle bundle) { + bundle = + new AppBundle64BitNativeLibrariesPreprocessor(command.getOutputPrintStream()) + .preprocess(bundle); + if (command.getLocalTestingMode()) { + bundle = new LocalTestingPreprocessor().preprocess(bundle); + } + bundle = new EntryCompressionPreprocessor().preprocess(bundle); + return bundle; + } + + private static LocalTestingInfo getLocalTestingInfo(AppBundle bundle) { + LocalTestingInfo.Builder localTestingInfo = LocalTestingInfo.newBuilder(); + bundle + .getBaseModule() + .getAndroidManifest() + .getMetadataValue(LocalTestingPreprocessor.METADATA_NAME) + .ifPresent( + localTestingPath -> + localTestingInfo.setEnabled(true).setLocalTestingPath(localTestingPath)); + return localTestingInfo.build(); + } + private static class ApksToGenerate { private final AppBundle appBundle; private final ApkBuildMode apkBuildMode; diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java index be0079d9..363c112f 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java @@ -26,7 +26,6 @@ import com.android.bundle.Commands.AssetModuleMetadata; import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; -import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Devices.DeviceSpec; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; @@ -169,26 +168,7 @@ ImmutableList execute(PrintStream output) { BuildApksResult toc = ResultUtils.readTableOfContents(getApksArchivePath()); Optional> requestedModuleNames = - getModules() - .map( - modules -> - modules.contains(ALL_MODULES_SHORTCUT) - ? Stream.concat( - toc.getVariantList().stream() - .flatMap(variant -> variant.getApkSetList().stream()) - .map(apkSet -> apkSet.getModuleMetadata().getName()), - toc.getAssetSliceSetList().stream() - .filter( - sliceSet -> - sliceSet - .getAssetModuleMetadata() - .getDeliveryType() - .equals(DeliveryType.INSTALL_TIME)) - .map(AssetSliceSet::getAssetModuleMetadata) - .map(AssetModuleMetadata::getName)) - .collect(toImmutableSet()) - : modules); - validateAssetModules(toc, requestedModuleNames); + getModules().map(modules -> resolveRequestedModules(modules, toc)); ApkMatcher apkMatcher = new ApkMatcher(getDeviceSpec(), requestedModuleNames, getInstant()); ImmutableList matchedApks = apkMatcher.getMatchingApks(toc); @@ -207,6 +187,20 @@ ImmutableList execute(PrintStream output) { } } + static ImmutableSet resolveRequestedModules( + ImmutableSet requestedModules, BuildApksResult toc) { + return requestedModules.contains(ALL_MODULES_SHORTCUT) + ? Stream.concat( + toc.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .map(apkSet -> apkSet.getModuleMetadata().getName()), + toc.getAssetSliceSetList().stream() + .map(AssetSliceSet::getAssetModuleMetadata) + .map(AssetModuleMetadata::getName)) + .collect(toImmutableSet()) + : requestedModules; + } + private void validateInput() { if (getModules().isPresent() && getModules().get().isEmpty()) { throw new ValidationException("The set of modules cannot be empty."); @@ -223,33 +217,6 @@ private void validateInput() { } } - /** Check that none of the requested modules is an asset module that is not install-time. */ - private static void validateAssetModules( - BuildApksResult toc, Optional> requestedModuleNames) { - if (requestedModuleNames.isPresent()) { - ImmutableList requestedNonInstallTimeAssetModules = - toc.getAssetSliceSetList().stream() - .filter( - sliceSet -> - !sliceSet - .getAssetModuleMetadata() - .getDeliveryType() - .equals(DeliveryType.INSTALL_TIME)) - .map(AssetSliceSet::getAssetModuleMetadata) - .map(AssetModuleMetadata::getName) - .filter(requestedModuleNames.get()::contains) - .collect(toImmutableList()); - if (!requestedNonInstallTimeAssetModules.isEmpty()) { - throw ValidationException.builder() - .withMessage( - String.format( - "The following requested asset packs do not have install time delivery: %s.", - requestedNonInstallTimeAssetModules)) - .build(); - } - } - } - private ImmutableList extractMatchedApksFromApksArchive( ImmutableList matchedApkPaths) { Path outputDirectoryPath = 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 92b25907..7c1c7e8e 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -21,9 +21,13 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkDirectoryExists; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndExecutable; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; -import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.android.bundle.Commands.AssetModuleMetadata; +import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; +import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Devices.DeviceSpec; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; @@ -43,6 +47,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -58,8 +63,6 @@ public abstract class InstallApksCommand { private static final Flag DEVICE_ID_FLAG = Flag.string("device-id"); private static final Flag> MODULES_FLAG = Flag.stringSet("modules"); private static final Flag ALLOW_DOWNGRADE_FLAG = Flag.booleanFlag("allow-downgrade"); - private static final Flag PUSH_SPLITS_FLAG = Flag.string("push-splits-to"); - private static final Flag CLEAR_PUSH_PATH_FLAG = Flag.booleanFlag("clear-push-path"); private static final Flag ALLOW_TEST_ONLY_FLAG = Flag.booleanFlag("allow-test-only"); private static final String ANDROID_SERIAL_VARIABLE = "ANDROID_SERIAL"; @@ -77,10 +80,6 @@ public abstract class InstallApksCommand { public abstract boolean getAllowDowngrade(); - public abstract Optional getPushSplitsPath(); - - public abstract boolean getClearPushPath(); - public abstract boolean getAllowTestOnly(); abstract AdbServer getAdbServer(); @@ -88,7 +87,6 @@ public abstract class InstallApksCommand { public static Builder builder() { return new AutoValue_InstallApksCommand.Builder() .setAllowDowngrade(false) - .setClearPushPath(false) .setAllowTestOnly(false); } @@ -108,10 +106,6 @@ public abstract static class Builder { /** The caller is responsible for the lifecycle of the {@link AdbServer}. */ public abstract Builder setAdbServer(AdbServer adbServer); - public abstract Builder setPushSplitsPath(String pushSplitsPath); - - public abstract Builder setClearPushPath(boolean clearPushPath); - public abstract Builder setAllowTestOnly(boolean allowTestOnly); public abstract InstallApksCommand build(); @@ -145,8 +139,6 @@ public static InstallApksCommand fromFlags( Optional> modules = MODULES_FLAG.getValue(flags); Optional allowDowngrade = ALLOW_DOWNGRADE_FLAG.getValue(flags); - Optional pushSplits = PUSH_SPLITS_FLAG.getValue(flags); - Optional clearPushPath = CLEAR_PUSH_PATH_FLAG.getValue(flags); Optional allowTestOnly = ALLOW_TEST_ONLY_FLAG.getValue(flags); flags.checkNoUnknownFlags(); @@ -169,18 +161,12 @@ public void execute() { try (TempDirectory tempDirectory = new TempDirectory()) { DeviceSpec deviceSpec = new DeviceAnalyzer(adbServer).getDeviceSpec(getDeviceId()); + BuildApksResult toc = ResultUtils.readTableOfContents(getApksArchivePath()); - ExtractApksCommand.Builder extractApksCommand = - ExtractApksCommand.builder() - .setApksArchivePath(getApksArchivePath()) - .setDeviceSpec(deviceSpec); - - if (!Files.isDirectory(getApksArchivePath())) { - extractApksCommand.setOutputDirectory(tempDirectory.getPath()); - } - getModules().ifPresent(extractApksCommand::setModules); - final ImmutableList extractedApks = extractApksCommand.build().execute(); - + final ImmutableList apksToInstall = + getApksToInstall(toc, deviceSpec, tempDirectory.getPath()); + final ImmutableList apksToPush = + getApksToPushToStorage(toc, deviceSpec, tempDirectory.getPath()); AdbRunner adbRunner = new AdbRunner(adbServer); InstallOptions installOptions = InstallOptions.builder() @@ -190,65 +176,108 @@ public void execute() { if (getDeviceId().isPresent()) { adbRunner.run( - device -> device.installApks(extractedApks, installOptions), getDeviceId().get()); + device -> device.installApks(apksToInstall, installOptions), getDeviceId().get()); } else { - adbRunner.run(device -> device.installApks(extractedApks, installOptions)); + adbRunner.run(device -> device.installApks(apksToInstall, installOptions)); } - pushSplits(deviceSpec, extractApksCommand.build(), adbRunner); + if (!apksToPush.isEmpty()) { + pushSplits(apksToPush, toc, adbRunner); + } } } - private void pushSplits( - DeviceSpec baseSpec, ExtractApksCommand baseExtractCommand, AdbRunner adbRunner) { - if (!getPushSplitsPath().isPresent()) { - return; + /** Extracts the apks that will be installed. */ + private ImmutableList getApksToInstall( + BuildApksResult toc, DeviceSpec deviceSpec, Path output) { + ExtractApksCommand.Builder extractApksCommand = + ExtractApksCommand.builder() + .setApksArchivePath(getApksArchivePath()) + .setDeviceSpec(deviceSpec); + if (!Files.isDirectory(getApksArchivePath())) { + extractApksCommand.setOutputDirectory(output); } + ImmutableSet dynamicAssetModules = + toc.getAssetSliceSetList().stream() + .map(AssetSliceSet::getAssetModuleMetadata) + .filter(metadata -> metadata.getDeliveryType() != DeliveryType.INSTALL_TIME) + .map(AssetModuleMetadata::getName) + .collect(toImmutableSet()); + getModules() + .map(modules -> ExtractApksCommand.resolveRequestedModules(modules, toc)) + .map(modules -> Sets.difference(modules, dynamicAssetModules).immutableCopy()) + .ifPresent(extractApksCommand::setModules); + return extractApksCommand.build().execute(); + } - ExtractApksCommand.Builder extractApksCommand = ExtractApksCommand.builder(); - extractApksCommand.setApksArchivePath(baseExtractCommand.getApksArchivePath()); - baseExtractCommand.getOutputDirectory().ifPresent(extractApksCommand::setOutputDirectory); - - // We want to push all modules... - extractApksCommand.setModules(ImmutableSet.of(ExtractApksCommand.ALL_MODULES_SHORTCUT)); - // ... and all languages - BuildApksResult toc = ResultUtils.readTableOfContents(getApksArchivePath()); - ImmutableSet targetedLanguages = ResultUtils.getAllTargetedLanguages(toc); - final ImmutableList extractedApksForPush = - extractApksCommand - .setDeviceSpec(baseSpec.toBuilder().addAllSupportedLocales(targetedLanguages).build()) - .build() - .execute(); + /** + * Extracts the apks that will be pushed to storage in local testing mode. + * + *

This includes: + * + *

    + *
  • Non-master splits of the base module that match the device targeting. + *
  • All feature modules splits that match the device targeting. + *
  • All non install-time asset modules splits that match the device targeting. + *
  • All language splits. + *
+ */ + private ImmutableList getApksToPushToStorage( + BuildApksResult toc, DeviceSpec deviceSpec, Path output) { + if (!toc.getLocalTestingInfo().getEnabled()) { + return ImmutableList.of(); + } + ExtractApksCommand.Builder extractApksCommand = + ExtractApksCommand.builder() + .setApksArchivePath(getApksArchivePath()) + .setDeviceSpec(addAllSupportedLanguages(deviceSpec, toc)); + if (!Files.isDirectory(getApksArchivePath())) { + extractApksCommand.setOutputDirectory(output); + } + ImmutableSet installTimeAssetModules = + toc.getAssetSliceSetList().stream() + .map(AssetSliceSet::getAssetModuleMetadata) + .filter(metadata -> metadata.getDeliveryType() == DeliveryType.INSTALL_TIME) + .map(AssetModuleMetadata::getName) + .collect(toImmutableSet()); + ImmutableSet allModules = + ExtractApksCommand.resolveRequestedModules( + ImmutableSet.of(ExtractApksCommand.ALL_MODULES_SHORTCUT), toc); + extractApksCommand.setModules( + Sets.difference(allModules, installTimeAssetModules).immutableCopy()); + return extractApksCommand.build().execute().stream() + .filter( + apk -> + !ResultUtils.getAllBaseMasterSplitPaths(toc).contains(apk.getFileName().toString())) + .collect(toImmutableList()); + } + private void pushSplits(ImmutableList splits, BuildApksResult toc, AdbRunner adbRunner) { + String packageName = toc.getPackageName(); + if (packageName.isEmpty()) { + throw new CommandExecutionException( + "Unable to determine the package name of the base APK. If your APK set was produced" + + " using an older version of bundletool, please regenerate it."); + } Device.PushOptions.Builder pushOptions = Device.PushOptions.builder() - .setDestinationPath(getPushSplitsPath().get()) - .setClearDestinationPath(getClearPushPath()); - - // We're going to need the package name later for pushing to relative paths - // (i.e. inside the app's private directory) - if (!getPushSplitsPath().get().startsWith("/")) { - String packageName = toc.getPackageName(); - if (packageName.isEmpty()) { - throw new CommandExecutionException( - "Unable to determine the package name of the base APK. If your APK " - + "set was produced using an older version of bundletool, please " - + "regenerate it. Alternatively, you can try again with an " - + "absolute path for --push-splits-to, pointing to a location " - + "that is writeable by the shell user, e.g. /sdcard/..."); - } - pushOptions.setPackageName(packageName); - } + .setDestinationPath(toc.getLocalTestingInfo().getLocalTestingPath()) + .setClearDestinationPath(true) + .setPackageName(packageName); if (getDeviceId().isPresent()) { - adbRunner.run( - device -> device.pushApks(extractedApksForPush, pushOptions.build()), - getDeviceId().get()); + adbRunner.run(device -> device.pushApks(splits, pushOptions.build()), getDeviceId().get()); } else { - adbRunner.run(device -> device.pushApks(extractedApksForPush, pushOptions.build())); + adbRunner.run(device -> device.pushApks(splits, pushOptions.build())); } } + /** Adds all supported languages in the given {@link BuildApksResult} to a {@link DeviceSpec}. */ + private static DeviceSpec addAllSupportedLanguages(DeviceSpec deviceSpec, BuildApksResult toc) { + ImmutableSet targetedLanguages = ResultUtils.getAllTargetedLanguages(toc); + return deviceSpec.toBuilder().addAllSupportedLocales(targetedLanguages).build(); + } + private void validateInput() { if (Files.isDirectory(getApksArchivePath())) { checkDirectoryExists(getApksArchivePath()); @@ -256,20 +285,6 @@ private void validateInput() { checkFileExistsAndReadable(getApksArchivePath()); } checkFileExistsAndExecutable(getAdbPath()); - if (getClearPushPath()) { - checkArgument( - getPushSplitsPath().isPresent(), - "--%s only applies when --%s is set.", - CLEAR_PUSH_PATH_FLAG.getName(), - PUSH_SPLITS_FLAG.getName()); - } - getPushSplitsPath() - .ifPresent( - path -> - checkArgument( - !path.isEmpty(), - "The value of the flag --%s cannot be empty.", - PUSH_SPLITS_FLAG.getName())); } public static CommandHelp help() { 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 67bcf4bb..f206b3a2 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java @@ -281,8 +281,7 @@ private void checkCompatibleWithApkTargetingHelper( private ImmutableList getMatchingApksFromAssetModules(BuildApksResult buildApksResult) { ImmutableList.Builder matchedApksBuilder = ImmutableList.builder(); - Predicate assetModuleNameMatcher = - getInstallTimeAssetModuleNameMatcher(buildApksResult); + Predicate assetModuleNameMatcher = getAssetModuleNameMatcher(buildApksResult); for (AssetSliceSet sliceSet : buildApksResult.getAssetSliceSetList()) { String moduleName = sliceSet.getAssetModuleMetadata().getName(); @@ -297,7 +296,11 @@ private ImmutableList getMatchingApksFromAssetModules(BuildApksResult b return matchedApksBuilder.build(); } - private Predicate getInstallTimeAssetModuleNameMatcher(BuildApksResult buildApksResult) { + private Predicate getAssetModuleNameMatcher(BuildApksResult buildApksResult) { + if (requestedModuleNames.isPresent()) { + return requestedModuleNames.get()::contains; + } + ImmutableSet upfrontAssetModuleNames = buildApksResult.getAssetSliceSetList().stream() .filter( @@ -309,8 +312,6 @@ private Predicate getInstallTimeAssetModuleNameMatcher(BuildApksResult b .map(sliceSet -> sliceSet.getAssetModuleMetadata().getName()) .collect(toImmutableSet()); - return requestedModuleNames.isPresent() - ? Sets.intersection(upfrontAssetModuleNames, requestedModuleNames.get())::contains - : upfrontAssetModuleNames::contains; + return upfrontAssetModuleNames::contains; } } diff --git a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java index 8aabcec1..470e5b99 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java @@ -34,7 +34,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.errorprone.annotations.FormatMethod; -import com.google.errorprone.annotations.FormatString; import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -157,9 +156,8 @@ public void pushApks(ImmutableList apks, PushOptions pushOptions) { try { // There are two different flows, depending on if the path is absolute or not... - if (!pushOptions.getDestinationPath().startsWith("/")) { - // Path is relative, so we're going to try to push it to the app's private dir - // For that, we will need the package name to use with "run-as" command + if (!splitsPath.startsWith("/")) { + // Path is relative, so we're going to try to push it to the app's external dir String packageName = pushOptions .getPackageName() @@ -168,63 +166,29 @@ public void pushApks(ImmutableList apks, PushOptions pushOptions) { new CommandExecutionException( "PushOptions.packageName must be set for relative paths.")); - // Some clean up first. Remove the destination dir if flag is set... - if (pushOptions.getClearDestinationPath()) { - commandExecutor.executeAsUserAndPrint(packageName, "rm -rf %s", splitsPath); - } - - // ...and recreate it, making sure the destination dir is empty. - // We don't want splits from previous runs in the directory. - // There isn't a nice way to test if dir is empty in shell, but rmdir will return error - commandExecutor.executeAsUserAndPrint( - packageName, - "mkdir -p %s && rmdir %s && mkdir -p %s", - splitsPath, - splitsPath, - splitsPath); - - // The push command further down doesn't support "run-as", so we're going to push - // to a temporary folder, then copy the files to the final destination - String remoteTmpPath = joinUnixPaths("/data/local/tmp/", packageName); - commandExecutor.executeAndPrint("mkdir -p %s", remoteTmpPath); - for (Path apkPath : apks) { - String remoteTmpFilePath = joinUnixPaths(remoteTmpPath, apkPath.getFileName().toString()); - device.pushFile(apkPath.toFile().getAbsolutePath(), remoteTmpFilePath); - - // Now we can copy ("cat" in case there is no "cp" on device) from tmp to splitsPath - // "sh -c" needed to wrap the whole "cat" call because of ">" redirect - commandExecutor.executeAsUserAndPrint( - packageName, - "cat %s > %s", - remoteTmpFilePath, - joinUnixPaths(splitsPath, apkPath.getFileName().toString())); - commandExecutor.executeAndPrint("rm %s", remoteTmpFilePath); - System.err.printf( - "Pushed \"%s\"%n", joinUnixPaths(splitsPath, apkPath.getFileName().toString())); - } - } else { - // Path is absolute. We assume it's pointing to a location writeable by ADB shell, - // which is explained in the bundletool help. It shouldn't point to app's private directory. - - // Some clean up first. Remove the destination dir if flag is set... - if (pushOptions.getClearDestinationPath()) { - commandExecutor.executeAndPrint("rm -rf %s", splitsPath); - } - - // ... and recreate it, making sure the destination dir is empty. - // We don't want splits from previous runs in the directory. - // There isn't a nice way to test if dir is empty in shell, but rmdir will return error - commandExecutor.executeAndPrint( - "mkdir -p %s && rmdir %s && mkdir -p %s", splitsPath, splitsPath, splitsPath); - - // Try to push files normally. Will fail if ADB shell doesn't have permission to write. - for (Path path : apks) { - device.pushFile( - path.toFile().getAbsolutePath(), - joinUnixPaths(splitsPath, path.getFileName().toString())); - System.err.printf( - "Pushed \"%s\"%n", joinUnixPaths(splitsPath, path.getFileName().toString())); - } + splitsPath = joinUnixPaths("/sdcard/Android/data/", packageName, "files", splitsPath); + } + // Now the path is absolute. We assume it's pointing to a location writeable by ADB shell. + // It shouldn't point to app's private directory. + + // Some clean up first. Remove the destination dir if flag is set... + if (pushOptions.getClearDestinationPath()) { + commandExecutor.executeAndPrint("rm -rf %s", splitsPath); + } + + // ... and recreate it, making sure the destination dir is empty. + // We don't want splits from previous runs in the directory. + // There isn't a nice way to test if dir is empty in shell, but rmdir will return error + commandExecutor.executeAndPrint( + "mkdir -p %s && rmdir %s && mkdir -p %s", splitsPath, splitsPath, splitsPath); + + // Try to push files normally. Will fail if ADB shell doesn't have permission to write. + for (Path path : apks) { + device.pushFile( + path.toFile().getAbsolutePath(), + joinUnixPaths(splitsPath, path.getFileName().toString())); + System.err.printf( + "Pushed \"%s\"%n", joinUnixPaths(splitsPath, path.getFileName().toString())); } } catch (IOException | TimeoutException @@ -234,9 +198,8 @@ public void pushApks(ImmutableList apks, PushOptions pushOptions) { throw CommandExecutionException.builder() .withCause(e) .withMessage( - "Pushing additional splits for offline testing of dynamic features failed. Your app" - + " might still have been installed correctly, but you won't be able to test" - + " with FakeSplitInstallManager.") + "Pushing additional splits for local testing failed. Your app might still have been" + + " installed correctly, but you won't be able to test dynamic modules.") .build(); } } @@ -287,17 +250,6 @@ private void executeAndPrint(String commandFormat, String... args) } } - /** Runs the command as the user of the debuggable app with specified package name. */ - @FormatMethod - private void executeAsUserAndPrint( - String packageName, @FormatString String command, String... args) - throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, - IOException { - // "run-as packageName" lets us run with the permissions of the app to access its directory - // "sh -c 'cmd'" is needed to be able to use > redirects and && operator inside the cmd - executeAndPrint("run-as %s sh -c %s", packageName, formatCommandWithArgs(command, args)); - } - /** Returns the string in single quotes, with any single quotes in the string escaped. */ static String escapeAndSingleQuote(String string) { return "'" + string.replace("'", "'\\''") + "'"; diff --git a/src/main/java/com/android/tools/build/bundletool/device/Device.java b/src/main/java/com/android/tools/build/bundletool/device/Device.java index 171bd6d0..d30b470f 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/Device.java +++ b/src/main/java/com/android/tools/build/bundletool/device/Device.java @@ -102,7 +102,7 @@ public abstract static class Builder { } } - /** Options related to APK installation. */ + /** Options related to pushing files to the device. */ @Immutable @AutoValue @AutoValue.CopyAnnotations @@ -118,10 +118,10 @@ public abstract static class PushOptions { public static Builder builder() { return new AutoValue_Device_PushOptions.Builder() .setTimeout(DEFAULT_ADB_TIMEOUT) - .setClearDestinationPath(false); + .setClearDestinationPath(true); } - /** Builder for {@link InstallOptions}. */ + /** Builder for {@link PushOptions}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setDestinationPath(String destinationPath); 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 8ad06a2b..26b1c470 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerHelper.java @@ -16,7 +16,6 @@ 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.ASSETS_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.ROOT_DIRECTORY; @@ -43,7 +42,6 @@ import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; import com.android.tools.build.bundletool.model.ModuleEntry; 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.model.WearApkLocator; import com.android.tools.build.bundletool.model.ZipPath; @@ -237,7 +235,6 @@ private void writeCompressedApkToZipFile(Path apkPath, Path outputApkGzipPath) { */ private Path writeProtoApk(ModuleSplit split, Path outputPath, Path tempDir) { boolean extractNativeLibs = split.getAndroidManifest().getExtractNativeLibsValue().orElse(true); - boolean isAssetSlice = split.getSplitType().equals(SplitType.ASSET_SLICE); // Embedded Wear 1.x APKs are supposed to be under res/raw/* Optional wear1ApkPath = WearApkLocator.findEmbeddedWearApkPath(split); @@ -253,7 +250,6 @@ private Path writeProtoApk(ModuleSplit split, Path outputPath, Path tempDir) { entryOptionForPath( pathInApk, /* uncompressNativeLibs= */ !extractNativeLibs, - /* splitIsAssetSlice= */ isAssetSlice, /* entryShouldCompress= */ entry.shouldCompress()); if (signingConfig.isPresent() && wear1ApkPath.isPresent() @@ -288,9 +284,8 @@ private Path writeProtoApk(ModuleSplit split, Path outputPath, Path tempDir) { private EntryOption[] entryOptionForPath( ZipPath path, boolean uncompressNativeLibs, - boolean splitIsAssetSlice, boolean entryShouldCompress) { - if (shouldCompress(path, uncompressNativeLibs, splitIsAssetSlice, entryShouldCompress)) { + if (shouldCompress(path, uncompressNativeLibs, entryShouldCompress)) { return new EntryOption[] {}; } else { return new EntryOption[] {EntryOption.UNCOMPRESSED}; @@ -300,7 +295,6 @@ private EntryOption[] entryOptionForPath( private boolean shouldCompress( ZipPath path, boolean uncompressNativeLibs, - boolean splitIsAssetSlice, boolean entryShouldCompress) { if (uncompressedPathMatchers.stream() .anyMatch(pathMatcher -> pathMatcher.matches(path.toString()))) { @@ -312,11 +306,6 @@ private boolean shouldCompress( return false; } - // Asset entries from asset slices should be uncompressed. - if (splitIsAssetSlice && path.startsWith(ASSETS_DIRECTORY)) { - return false; - } - // Common extensions that should remain uncompressed because compression doesn't provide any // gains. if (!NO_DEFAULT_UNCOMPRESS_EXTENSIONS.enabledForVersion(bundleVersion) @@ -336,7 +325,6 @@ private boolean shouldCompress( /** Takes the given APK and adds the files that weren't processed by AAPT2. */ private void addNonAapt2Files(ZFile zFile, ModuleSplit split) throws IOException { boolean extractNativeLibs = split.getAndroidManifest().getExtractNativeLibsValue().orElse(true); - boolean isAssetSlice = split.getSplitType().equals(SplitType.ASSET_SLICE); // Add the non-Aapt2 files. for (ModuleEntry entry : split.getEntries()) { @@ -346,7 +334,7 @@ private void addNonAapt2Files(ZFile zFile, ModuleSplit split) throws IOException zFile.add( pathInApk.toString(), entryInputStream, - shouldCompress(pathInApk, !extractNativeLibs, isAssetSlice, entry.shouldCompress())); + shouldCompress(pathInApk, !extractNativeLibs, entry.shouldCompress())); } } } 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 baa1ad17..6600dfa4 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -34,6 +34,7 @@ import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.InstantMetadata; +import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Commands.Variant; import com.android.bundle.Config.Bundletool; import com.android.bundle.Devices.DeviceSpec; @@ -96,7 +97,8 @@ public void populateApkSetBuilder( GeneratedApks generatedApks, GeneratedAssetSlices generatedAssetSlices, ApkBuildMode apkBuildMode, - Optional deviceSpec) { + Optional deviceSpec, + LocalTestingInfo localTestingInfo) { ImmutableList allVariantsWithTargeting = serializeApks(generatedApks, apkBuildMode, deviceSpec); ImmutableList allAssetSliceSets = @@ -111,6 +113,7 @@ public void populateApkSetBuilder( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addAllAssetSliceSet(allAssetSliceSets) + .setLocalTestingInfo(localTestingInfo) .build()); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java index 7bcec9ca..b784f8a2 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -62,8 +62,6 @@ public abstract class AndroidManifest { public static final String APPLICATION_ELEMENT_NAME = "application"; public static final String META_DATA_ELEMENT_NAME = "meta-data"; - public static final String SUPPORTS_GL_TEXTURE_ELEMENT_NAME = "supports-gl-texture"; - public static final String USES_FEATURE_ELEMENT_NAME = "uses-feature"; public static final String USES_SDK_ELEMENT_NAME = "uses-sdk"; public static final String ACTIVITY_ELEMENT_NAME = "activity"; public static final String SERVICE_ELEMENT_NAME = "service"; @@ -71,7 +69,6 @@ public abstract class AndroidManifest { public static final String DEBUGGABLE_ATTRIBUTE_NAME = "debuggable"; public static final String EXTRACT_NATIVE_LIBS_ATTRIBUTE_NAME = "extractNativeLibs"; - public static final String GL_VERSION_ATTRIBUTE_NAME = "glEsVersion"; public static final String ICON_ATTRIBUTE_NAME = "icon"; public static final String MAX_SDK_VERSION_ATTRIBUTE_NAME = "maxSdkVersion"; public static final String MIN_SDK_VERSION_ATTRIBUTE_NAME = "minSdkVersion"; @@ -106,7 +103,6 @@ public abstract class AndroidManifest { public static final int VERSION_CODE_RESOURCE_ID = 0x0101021b; public static final int VERSION_NAME_RESOURCE_ID = 0x0101021c; public static final int IS_FEATURE_SPLIT_RESOURCE_ID = 0x0101055b; - public static final int GL_ES_VERSION_RESOURCE_ID = 0x01010281; public static final int TARGET_SANDBOX_VERSION_RESOURCE_ID = 0x0101054c; public static final int SPLIT_NAME_RESOURCE_ID = 0x01010549; public static final int INSTALL_LOCATION_RESOURCE_ID = 0x010102b7; 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 3a13bd7f..48b3f44b 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -84,9 +84,13 @@ public static AppBundle buildFromModules( .map(ResourceId::create) .collect(toImmutableSet()); + ImmutableSet pinnedResourceNames = + ImmutableSet.copyOf(bundleConfig.getMasterResources().getResourceNamesList()); + return builder() .setModules(Maps.uniqueIndex(modules, BundleModule::getName)) - .setMasterPinnedResources(pinnedResourceIds) + .setMasterPinnedResourceIds(pinnedResourceIds) + .setMasterPinnedResourceNames(pinnedResourceNames) .setBundleConfig(bundleConfig) .setBundleMetadata(bundleMetadata) .build(); @@ -94,8 +98,16 @@ public static AppBundle buildFromModules( public abstract ImmutableMap getModules(); - /** Resources that must remain in the master split regardless of their targeting configuration. */ - public abstract ImmutableSet getMasterPinnedResources(); + /** + * Resource IDs that must remain in the master split regardless of their targeting configuration. + */ + public abstract ImmutableSet getMasterPinnedResourceIds(); + + /** + * Resource names that must remain in the master split regardless of their targeting + * configuration. + */ + public abstract ImmutableSet getMasterPinnedResourceNames(); public abstract BundleConfig getBundleConfig(); @@ -294,7 +306,9 @@ public Builder setRawModules(Collection bundleModules) { return this; } - public abstract Builder setMasterPinnedResources(ImmutableSet pinnedResourceIds); + public abstract Builder setMasterPinnedResourceIds(ImmutableSet pinnedResourceIds); + + public abstract Builder setMasterPinnedResourceNames(ImmutableSet pinnedResourceNames); public abstract Builder setBundleConfig(BundleConfig bundleConfig); diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index f7db455d..e33390a2 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -21,8 +21,6 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.APPLICATION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.EXTRACT_NATIVE_LIBS_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.EXTRACT_NATIVE_LIBS_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.GL_ES_VERSION_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.GL_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.HAS_CODE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_ATTRIBUTE_NAME; @@ -40,11 +38,9 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.RESOURCE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.SUPPORTS_GL_TEXTURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_CODE_RESOURCE_ID; @@ -67,7 +63,6 @@ /** Modifies the manifest in the protocol buffer format. */ public class ManifestEditor { - private static final int OPEN_GL_VERSION_MULTIPLIER = 0x10000; private static final ImmutableList SPLIT_NAME_ELEMENT_NAMES = ImmutableList.of(ACTIVITY_ELEMENT_NAME, SERVICE_ELEMENT_NAME, PROVIDER_ELEMENT_NAME); @@ -190,7 +185,6 @@ private ManifestEditor addMetaDataValue(String key, XmlProtoAttributeBuilder val return this; } - /** * Sets the 'android:extractNativeLibs' value in the {@code application} tag. * 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 5c26ff93..c6d13261 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java @@ -22,6 +22,9 @@ import static com.android.tools.build.bundletool.model.BundleModule.LIB_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.RESOURCES_DIRECTORY; import static com.android.tools.build.bundletool.model.BundleModule.ROOT_DIRECTORY; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_CERT_SHA256_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_TYPE_METADATA_KEY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.SCREEN_DENSITY_TO_PROTO_VALUE_MAP; import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeApkTargeting; import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeVariantTargeting; @@ -51,6 +54,8 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.bundle.Targeting.VulkanVersion; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; +import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.utils.CertificateHelper; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; @@ -62,6 +67,9 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.errorprone.annotations.Immutable; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; import java.util.StringJoiner; @@ -261,6 +269,27 @@ public ModuleSplit removeUnknownSplitComponents(ImmutableSet knownSplits return toBuilder().setAndroidManifest(apkManifest).build(); } + /** Writes the source stamp in the split manifest. */ + public ModuleSplit writeStampInManifest(SourceStamp sourceStamp, StampType stampType) + throws CertificateEncodingException, NoSuchAlgorithmException { + if (!isBaseModuleSplit() || !isMasterSplit()) { + return this; + } + + ImmutableList certificates = + sourceStamp.getSigningConfiguration().getCertificates(); + // Computing the hash of the leaf certificate only. + String stampCertificateSha256 = CertificateHelper.sha256AsHexString(certificates.get(0)); + AndroidManifest apkManifest = + getAndroidManifest() + .toEditor() + .addMetaDataString(STAMP_SOURCE_METADATA_KEY, sourceStamp.getSource()) + .addMetaDataString(STAMP_TYPE_METADATA_KEY, stampType.toString()) + .addMetaDataString(STAMP_CERT_SHA256_METADATA_KEY, stampCertificateSha256) + .save(); + return toBuilder().setAndroidManifest(apkManifest).build(); + } + /** Writes the final manifest that reflects the Split ID. */ @CheckReturnValue public ModuleSplit writeSplitIdInManifest(String resolvedSplitIdSuffix) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/SourceStamp.java b/src/main/java/com/android/tools/build/bundletool/model/SourceStamp.java new file mode 100644 index 00000000..ede4e425 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/SourceStamp.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; + +/** + * Stamp improves traceability of apps with respect to unauthorized distribution. + * + *

The stamp is part of the APK that is protected by the signing block. + * + *

The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + */ +@Immutable +@AutoValue +public abstract class SourceStamp { + + public static final String LOCAL_SOURCE = "local-unstamped"; + + public static final String STAMP_SOURCE_METADATA_KEY = "com.google.android.stamp.source"; + public static final String STAMP_TYPE_METADATA_KEY = "com.google.android.stamp.type"; + public static final String STAMP_CERT_SHA256_METADATA_KEY = + "com.google.android.stamp.stamp-cert-sha256"; + + /** Returns the signing configuration used for signing the stamp. */ + public abstract SigningConfiguration getSigningConfiguration(); + + /** + * Returns the name of source generating the stamp for the APK. + * + *

For stores, it is their package names. + * + *

For local stamps, it is "local-unstamped". Local stamps are unverifiable. + */ + public abstract String getSource(); + + public static Builder builder() { + return new AutoValue_SourceStamp.Builder().setSource(LOCAL_SOURCE); + } + + /** Builder of {@link SourceStamp} instances. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setSigningConfiguration(SigningConfiguration signingConfiguration); + + public abstract Builder setSource(String source); + + public abstract SourceStamp build(); + } + + /** Type of stamp generated. */ + public enum StampType { + // Stamp generated for all APKs except universal APKs. + STAMP_TYPE_DEFAULT, + // Stamp generated for a universal APK. + STAMP_TYPE_UNIVERSAL, + // Stamp generated for a local APK, regardless of the APK type. + STAMP_TYPE_LOCAL, + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java index 14655a3a..57c2da29 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java @@ -18,7 +18,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.MoreCollectors.toOptional; +import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.ValidationException; import com.google.auto.value.AutoValue; @@ -26,6 +28,7 @@ import com.google.common.collect.Iterables; import com.google.errorprone.annotations.Immutable; import java.util.HashSet; +import java.util.Optional; import java.util.Set; /** @@ -70,6 +73,21 @@ public String getSubPathBaseName(int maxIndex) { .toString(); } + /** + * Returns the value of the targeting for the given dimension, if this dimension is targeted by + * this directory. + * + * @param dimension The dimension for which the targeting must be extracted. + * @return The targeting for the specified dimension, or an empty optional if not found. + */ + public Optional getTargeting(TargetingDimension dimension) { + // We're assuming that dimensions are not duplicated (see checkNoDuplicateDimensions). + return getPathSegments().stream() + .filter(segment -> segment.getTargetingDimension().equals(Optional.of(dimension))) + .map(segment -> segment.getTargeting()) + .collect(toOptional()); + } + /** * Returns a copy of the TargetedDirectory with a targeting dimension removed. * diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/CertificateHelper.java b/src/main/java/com/android/tools/build/bundletool/model/utils/CertificateHelper.java new file mode 100644 index 00000000..92537894 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/CertificateHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.model.utils; + +import com.google.common.io.BaseEncoding; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +/** Helpers related to dealing with certificates. */ +public final class CertificateHelper { + + private CertificateHelper() {} + + public static String sha256AsHexString(X509Certificate certificate) + throws CertificateEncodingException { + try { + return toHexString(getSha256Bytes(certificate.getEncoded())); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not found.", e); + } + } + + private static byte[] getSha256Bytes(byte[] input) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(input); + return messageDigest.digest(); + } + + /** Obtain hex encoded string from raw bytes. */ + private static String toHexString(byte[] rawBytes) { + return BaseEncoding.base16().upperCase().encode(rawBytes); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java index 8909ff01..9e4186a7 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java @@ -21,8 +21,10 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.android.bundle.Commands.ApkDescription; +import com.android.bundle.Commands.ApkSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.Variant; +import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.utils.files.BufferedIo; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -33,6 +35,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -162,5 +165,23 @@ public static ImmutableSet getAllTargetedLanguages(BuildApksResult resul .collect(toImmutableSet()); } + /** Return the paths for all the base master splits in the given {@link BuildApksResult}. */ + public static ImmutableSet getAllBaseMasterSplitPaths(BuildApksResult toc) { + return splitApkVariants(toc).stream() + .map(Variant::getApkSetList) + .flatMap(List::stream) + .filter( + apkSet -> + apkSet + .getModuleMetadata() + .getName() + .equals(BundleModuleName.BASE_MODULE_NAME.getName())) + .map(ApkSet::getApkDescriptionList) + .flatMap(List::stream) + .filter(apkDescription -> apkDescription.getSplitApkMetadata().getIsMasterSplit()) + .map(ApkDescription::getPath) + .collect(toImmutableSet()); + } + private ResultUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java index a419efa5..82c0ad88 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "0.12.0"; + private static final String CURRENT_VERSION = "0.13.0"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java new file mode 100644 index 00000000..394be2a1 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/EntryCompressionPreprocessor.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 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.preprocessors; + +import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModule.ModuleType; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import javax.annotation.CheckReturnValue; + +/** + * Preprocessor that overrides the compression for module entries. + * + *

Asset module entries are uncompressed. + */ +public class EntryCompressionPreprocessor implements AppBundlePreprocessor { + + @Override + public AppBundle preprocess(AppBundle bundle) { + return bundle.toBuilder() + .setRawModules(setEntryCompression(bundle.getModules().values())) + .build(); + } + + @CheckReturnValue + private static ImmutableList setEntryCompression( + ImmutableCollection modules) { + return modules.stream() + .map(EntryCompressionPreprocessor::setEntryCompression) + .collect(toImmutableList()); + } + + private static BundleModule setEntryCompression(BundleModule module) { + if (module.getModuleType().equals(ModuleType.ASSET_MODULE)) { + return module.toBuilder() + .setRawEntries( + module.getEntries().stream() + .map( + entry -> + entry.getPath().startsWith(ASSETS_DIRECTORY) + ? entry.setCompression(false) + : entry) + .collect(toImmutableList())) + .build(); + } + return module; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalTestingPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalTestingPreprocessor.java new file mode 100644 index 00000000..96f7fb75 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalTestingPreprocessor.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.preprocessors; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.google.common.annotations.VisibleForTesting; + +/** + * Preprocessor that injects local testing metadata into the base module's manifest. + * + *

The metadata element is {@code }. + */ +public class LocalTestingPreprocessor implements AppBundlePreprocessor { + public static final String METADATA_NAME = "local_testing_dir"; + @VisibleForTesting static final String METADATA_VALUE = "local_testing"; + + @Override + public AppBundle preprocess(AppBundle bundle) { + return bundle.toBuilder() + .setRawModules( + bundle.getModules().values().stream() + .map(module -> module.isBaseModule() ? addLocalTestingMetadata(module) : module) + .collect(toImmutableList())) + .build(); + } + + private static BundleModule addLocalTestingMetadata(BundleModule module) { + return module.toBuilder() + .setAndroidManifest(addLocalTestingMetadata(module.getAndroidManifest())) + .build(); + } + + private static AndroidManifest addLocalTestingMetadata(AndroidManifest manifest) { + if (manifest.getMetadataValue(METADATA_NAME).isPresent()) { + return manifest; + } + return manifest.toEditor().addMetaDataString(METADATA_NAME, METADATA_VALUE).save(); + } +} 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 e8f301d0..ebb7915b 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java @@ -47,8 +47,11 @@ public abstract class ApkGenerationConfiguration { */ public abstract ImmutableSet getAbisForPlaceholderLibs(); - /** Resources that are pinned to the master split. */ - public abstract ImmutableSet getMasterPinnedResources(); + /** Resources IDs that are pinned to the master split. */ + public abstract ImmutableSet getMasterPinnedResourceIds(); + + /** Resource names that are pinned to the master split. */ + public abstract ImmutableSet getMasterPinnedResourceNames(); /** Resources that are (transitively) reachable from AndroidManifest.xml of the base module. */ public abstract ImmutableSet getBaseManifestReachableResources(); @@ -72,7 +75,8 @@ public static Builder builder() { .setInstallableOnExternalStorage(false) .setAbisForPlaceholderLibs(ImmutableSet.of()) .setOptimizationDimensions(ImmutableSet.of()) - .setMasterPinnedResources(ImmutableSet.of()) + .setMasterPinnedResourceIds(ImmutableSet.of()) + .setMasterPinnedResourceNames(ImmutableSet.of()) .setBaseManifestReachableResources(ImmutableSet.of()) .setSuffixStrippings(ImmutableMap.of()); } @@ -99,7 +103,9 @@ public abstract Builder setEnableNativeLibraryCompressionSplitter( public abstract Builder setAbisForPlaceholderLibs(ImmutableSet abis); - public abstract Builder setMasterPinnedResources(ImmutableSet resourceIds); + public abstract Builder setMasterPinnedResourceIds(ImmutableSet resourceIds); + + public abstract Builder setMasterPinnedResourceNames(ImmutableSet resourceNames); public abstract Builder setBaseManifestReachableResources(ImmutableSet resourceIds); 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 96eeb825..3a35849d 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java @@ -206,8 +206,10 @@ public ModuleSplit makeInstantManifestChanges(ModuleSplit moduleSplit) { private SplittingPipeline createResourcesSplittingPipeline() { ImmutableList.Builder resourceSplitters = ImmutableList.builder(); - ImmutableSet masterPinnedResources = - apkGenerationConfiguration.getMasterPinnedResources(); + ImmutableSet masterPinnedResourceIds = + apkGenerationConfiguration.getMasterPinnedResourceIds(); + ImmutableSet masterPinnedResourceNames = + apkGenerationConfiguration.getMasterPinnedResourceNames(); ImmutableSet baseManifestReachableResources = apkGenerationConfiguration.getBaseManifestReachableResources(); @@ -217,7 +219,7 @@ private SplittingPipeline createResourcesSplittingPipeline() { resourceSplitters.add( new ScreenDensityResourcesSplitter( bundleVersion, - /* pinWholeResourceToMaster= */ masterPinnedResources::contains, + /* pinWholeResourceToMaster= */ masterPinnedResourceIds::contains, /* pinLowestBucketOfResourceToMaster= */ baseManifestReachableResources::contains)); } @@ -227,7 +229,8 @@ private SplittingPipeline createResourcesSplittingPipeline() { Predicate pinLangResourceToMaster = Predicates.or( // Resources that are unconditionally in the master split. - entry -> masterPinnedResources.contains(entry.getResourceId()), + entry -> masterPinnedResourceIds.contains(entry.getResourceId()), + entry -> masterPinnedResourceNames.contains(entry.getEntry().getName()), // Resources reachable from the AndroidManifest.xml should have at least one config // in the master split (ie. either the default config, or all configs). entry -> diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java index 9ae99ffc..b0bfd64f 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AbiParityValidator.java @@ -27,7 +27,7 @@ import com.google.common.collect.ImmutableSet; import java.util.Set; -/** Validates that all modules, that contain some native libraries, support the same set of ABIs. */ +/** Validates that all modules that contain some native libraries support the same set of ABIs. */ public class AbiParityValidator extends SubValidator { @Override diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java index ac01cf9c..cbce4b93 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java @@ -44,6 +44,7 @@ public class AppBundleValidator { // More specific file validations. new EntryClashValidator(), new AbiParityValidator(), + new TextureCompressionFormatParityValidator(), new DexFilesValidator(), new ApexBundleValidator(), // Targeting validations. diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java index 499c081e..e001c324 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java @@ -59,6 +59,7 @@ public class BundleModulesValidator { // More specific file validations. new EntryClashValidator(), new AbiParityValidator(), + new TextureCompressionFormatParityValidator(), new DexFilesValidator(), new ApexBundleValidator(), // Other. diff --git a/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java new file mode 100644 index 00000000..3e924a7d --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java @@ -0,0 +1,138 @@ +/* + * 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.validation; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.bundle.Targeting.TextureCompressionFormat; +import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ModuleEntry; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.android.tools.build.bundletool.model.targeting.TargetedDirectory; +import com.android.tools.build.bundletool.model.targeting.TargetingDimension; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Optional; + +/** + * Validate that all modules that contain directories with targeted texture formats support the same + * set of texture formats (including "fallback" directories for untargeted textures). + */ +public class TextureCompressionFormatParityValidator extends SubValidator { + + /** + * Represents a set of texture formats and the presence or not of directories with untargeted + * textures. + */ + @AutoValue + public abstract static class SupportedTextureCompressionFormats { + public static SupportedTextureCompressionFormats create( + ImmutableSet formats, boolean hasFallback) { + return new AutoValue_TextureCompressionFormatParityValidator_SupportedTextureCompressionFormats( + formats, hasFallback); + } + + public abstract ImmutableSet getFormats(); + + public abstract boolean getHasFallback(); + + @Override + public final String toString() { + return getFormats().toString() + + (getHasFallback() ? " (with fallback directories)" : " (without fallback directories)"); + } + } + + @Override + public void validateAllModules(ImmutableList modules) { + BundleModule referentialModule = null; + SupportedTextureCompressionFormats referentialTextureCompressionFormats = null; + for (BundleModule module : modules) { + SupportedTextureCompressionFormats moduleTextureCompressionFormats = + getSupportedTextureCompressionFormats(module); + + if (moduleTextureCompressionFormats.getFormats().isEmpty()) { + continue; + } + + if (referentialTextureCompressionFormats == null) { + referentialModule = module; + referentialTextureCompressionFormats = moduleTextureCompressionFormats; + } else if (!referentialTextureCompressionFormats.equals(moduleTextureCompressionFormats)) { + throw ValidationException.builder() + .withMessage( + "All modules with targeted textures must have the same set of texture formats, but" + + " module '%s' has formats %s and module '%s' has formats %s.", + referentialModule.getName(), + referentialTextureCompressionFormats, + module.getName(), + moduleTextureCompressionFormats) + .build(); + } + } + } + + private static SupportedTextureCompressionFormats getSupportedTextureCompressionFormats( + BundleModule module) { + // Extract targeted directories from entries (like done when generating assets targeting) + ImmutableSet targetedDirectories = + module + .findEntriesUnderPath(BundleModule.ASSETS_DIRECTORY) + .map(ModuleEntry::getPath) + .filter(path -> path.getNameCount() > 1) + .map(ZipPath::getParent) + .map(TargetedDirectory::parse) + .collect(toImmutableSet()); + + // Inspect the targetings to extract texture compression formats. + ImmutableSet formats = + targetedDirectories.stream() + .map(directory -> directory.getTargeting(TargetingDimension.TEXTURE_COMPRESSION_FORMAT)) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(targeting -> targeting.getTextureCompressionFormat().getValueList().stream()) + .map(TextureCompressionFormat::getAlias) + .collect(toImmutableSet()); + + // Check if one or more targeted directories have "fallback" sibling directories. + boolean hasFallback = + targetedDirectories.stream() + .anyMatch( + directory -> { + Optional targeting = + directory.getTargeting(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + if (targeting.isPresent()) { + // Check if a sibling folder without texture targeting exists. If yes, this is + // called a "fallback". + TargetedDirectory siblingFallbackDirectory = + directory.removeTargeting(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + return module + .findEntriesUnderPath(siblingFallbackDirectory.toZipPath()) + .findAny() + .isPresent(); + } + + return false; + }); + + return SupportedTextureCompressionFormats.create(formats, hasFallback); + } +} diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index c323e217..3cce9427 100644 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -20,6 +20,9 @@ message BuildApksResult { // List of the created asset slices. repeated AssetSliceSet asset_slice_set = 3; + + // Information about local testing mode. + LocalTestingInfo local_testing_info = 5; } // Variant is a group of APKs that covers a part of the device configuration @@ -171,3 +174,13 @@ message SystemApkMetadata { // Holds data specific to APEX APKs. message ApexApkMetadata {} + +message LocalTestingInfo { + // Indicates if the bundle is built in local testing mode. + bool enabled = 1; + // The local testing path, as specified in the base manifest. + // This refers to the relative path on the external directory of the app where + // APKs will be pushed for local testing. + // Set only if local testing is enabled. + string local_testing_path = 2; +} diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index 1a1332dc..509bb5a1 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -28,8 +28,10 @@ message Compression { // Resources to keep in the master split. message MasterResources { - // Resource IDs to be kept in master splits. + // Resource IDs to be kept in master split. repeated int32 resource_ids = 1; + // Resource names to be kept in master split. + repeated string resource_names = 2; } message Optimizations { 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 864af91f..5b699cfd 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -395,6 +395,38 @@ public void buildingViaFlagsAndBuilderHasSameResult_androidSerialVariable() thro assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test + public void buildingViaFlagsAndBuilderHasSameResult_optionalLocalTestingMode() throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildApksCommand commandViaFlags = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH, + // Optional values. + "--local-testing"), + new PrintStream(output), + systemEnvironmentProvider, + fakeAdbServer); + BuildApksCommand.Builder commandViaBuilder = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + // Optional values. + .setLocalTestingMode(true) + // Must copy instance of the internal executor service. + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()); + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + @Test public void outputNotSet_throws() throws Exception { expectMissingRequiredBuilderPropertyException( 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 c937f433..f9e8ddcc 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -98,7 +98,6 @@ 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.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset; @@ -166,6 +165,7 @@ import com.android.tools.build.bundletool.model.utils.files.FilePreconditions; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.preprocessors.LocalTestingPreprocessor; import com.android.tools.build.bundletool.testing.Aapt2Helper; import com.android.tools.build.bundletool.testing.ApkSetUtils; import com.android.tools.build.bundletool.testing.AppBundleBuilder; @@ -186,6 +186,7 @@ import com.google.common.io.Closer; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Int32Value; import java.io.ByteArrayOutputStream; import java.io.File; @@ -1369,8 +1370,7 @@ public void buildApksCommand_universal_generatesSingleApkWithAllTcfAssets() thro Variant universalVariant = standaloneApkVariants(result).get(0); assertThat(apkDescriptions(universalVariant)).hasSize(1); - com.android.bundle.Commands.ApkDescription universalApk = - apkDescriptions(universalVariant).get(0); + ApkDescription universalApk = apkDescriptions(universalVariant).get(0); // All assets from "feature_tcf_assets" are included inside the APK File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); @@ -1431,8 +1431,7 @@ public void buildApksCommand_universal_strip64BitLibraries_doesNotStrip() throws assertThat(universalVariant.getTargeting()).isEqualTo(UNRESTRICTED_VARIANT_TARGETING); assertThat(apkDescriptions(universalVariant)).hasSize(1); - com.android.bundle.Commands.ApkDescription universalApk = - apkDescriptions(universalVariant).get(0); + ApkDescription universalApk = apkDescriptions(universalVariant).get(0); assertThat(universalApk.getTargeting()).isEqualToDefaultInstance(); File universalApkFile = extractFromApkSetFile(apkSetFile, universalApk.getPath(), outputDir); @@ -2305,8 +2304,7 @@ public void buildApksCommand_standalone_noTextureTargeting() throws Exception { assertThat(standaloneApkVariants(result)).hasSize(1); assertThat(apkDescriptions(standaloneApkVariants(result))).hasSize(1); - com.android.bundle.Commands.ApkDescription shard = - apkDescriptions(standaloneApkVariants(result)).get(0); + ApkDescription shard = apkDescriptions(standaloneApkVariants(result)).get(0); assertThat(apkSetFile).hasFile(shard.getPath()); try (ZipFile shardZip = @@ -3553,7 +3551,7 @@ public void apkModifier_modifyingVersionCode() throws Exception { new ApkModifier() { @Override public AndroidManifest modifyManifest( - AndroidManifest manifest, ApkDescription apkDescription) { + AndroidManifest manifest, ApkModifier.ApkDescription apkDescription) { return manifest .toEditor() .setVersionCode(1000 + apkDescription.getVariantNumber()) @@ -4127,7 +4125,7 @@ public void allApksSignedWithV1_minSdkLessThan24() throws Exception { try (ZipFile apkSet = new ZipFile(apkSetPath.toFile())) { BuildApksResult result = extractTocFromApkSetFile(apkSet, outputDir); ImmutableList apkDescriptions = apkDescriptions(result.getVariantList()); - checkState(apkDescriptions.size() > 0); + assertThat(apkDescriptions).isNotEmpty(); for (ApkDescription apkDescription : apkDescriptions) { ImmutableSet filesInApk = filesInApk(apkDescription, apkSet); assertThat(filesInApk).contains("META-INF/CERT.RSA"); @@ -4166,7 +4164,7 @@ public void allApksSignedWithV1_minSdkAtLeast24_oldBundletool() throws Exception try (ZipFile apkSet = new ZipFile(apkSetPath.toFile())) { BuildApksResult result = extractTocFromApkSetFile(apkSet, outputDir); ImmutableList apkDescriptions = apkDescriptions(result.getVariantList()); - checkState(apkDescriptions.size() > 0); + assertThat(apkDescriptions).isNotEmpty(); for (ApkDescription apkDescription : apkDescriptions) { ImmutableSet filesInApk = filesInApk(apkDescription, apkSet); assertThat(filesInApk).contains("META-INF/CERT.RSA"); @@ -4205,7 +4203,7 @@ public void allApksNotSignedWithV1_minSdkAtLeast24_recentBundletool() throws Exc try (ZipFile apkSet = new ZipFile(apkSetPath.toFile())) { BuildApksResult result = extractTocFromApkSetFile(apkSet, outputDir); ImmutableList apkDescriptions = apkDescriptions(result.getVariantList()); - checkState(apkDescriptions.size() > 0); + assertThat(apkDescriptions).isNotEmpty(); for (ApkDescription apkDescription : apkDescriptions) { ImmutableSet filesInApk = filesInApk(apkDescription, apkSet); assertThat(filesInApk).doesNotContain("META-INF/CERT.RSA"); @@ -4213,6 +4211,141 @@ public void allApksNotSignedWithV1_minSdkAtLeast24_recentBundletool() throws Exc } } + @Test + public void localTestingMode_enabled_addsMetadata() throws Exception { + AppBundle appBundle = createAppBundleWithBaseAndFeatureModules("feature"); + Path bundlePath = createAndStoreBundle(appBundle); + + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse("--bundle=" + bundlePath, "--output=" + outputFilePath, "--local-testing"), + fakeAdbServer); + + Path apkSetPath = execute(command); + + try (ZipFile apkSet = new ZipFile(apkSetPath.toFile())) { + BuildApksResult result = extractTocFromApkSetFile(apkSet, outputDir); + assertThat(result.hasLocalTestingInfo()).isTrue(); + assertThat(result.getLocalTestingInfo().getEnabled()).isTrue(); + assertThat(result.getLocalTestingInfo().getLocalTestingPath()).isNotEmpty(); + ImmutableList apkDescriptions = apkDescriptions(result.getVariantList()); + assertThat(apkDescriptions).isNotEmpty(); + assertThat(apkDescriptions.stream().map(ApkDescription::getPath)) + .contains("splits/base-master.apk"); + for (ApkDescription apkDescription : apkDescriptions) { + File apk = extractFromApkSetFile(apkSet, apkDescription.getPath(), outputDir); + // The local testing metadata is set if and only if the apk is the base master. + assertThat( + (apkDescription.hasSplitApkMetadata() + && apkDescription.getSplitApkMetadata().getSplitId().isEmpty() + && apkDescription.getSplitApkMetadata().getIsMasterSplit()) + || apkDescription.hasStandaloneApkMetadata()) + .isEqualTo( + extractAndroidManifest(apk) + .getMetadataValue(LocalTestingPreprocessor.METADATA_NAME) + .isPresent()); + } + } + } + + @Test + public void localTestingMode_disabled_doesNotAddMetadata() throws Exception { + AppBundle appBundle = createAppBundleWithBaseAndFeatureModules("feature"); + Path bundlePath = createAndStoreBundle(appBundle); + + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser().parse("--bundle=" + bundlePath, "--output=" + outputFilePath), + fakeAdbServer); + + Path apkSetPath = execute(command); + + try (ZipFile apkSet = new ZipFile(apkSetPath.toFile())) { + BuildApksResult result = extractTocFromApkSetFile(apkSet, outputDir); + assertThat(result.getLocalTestingInfo().getEnabled()).isFalse(); + ImmutableList apkDescriptions = apkDescriptions(result.getVariantList()); + assertThat(apkDescriptions).isNotEmpty(); + for (ApkDescription apkDescription : apkDescriptions) { + File apk = extractFromApkSetFile(apkSet, apkDescription.getPath(), outputDir); + assertThat( + extractAndroidManifest(apk) + .getMetadataValue(LocalTestingPreprocessor.METADATA_NAME)) + .isEmpty(); + } + } + } + + @Test + public void buildApksCommand_overridesAssetModuleCompression() throws Exception { + byte[] dummyContent = new byte[100]; + + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex", dummyContent) + .addFile("assets/images/image.jpg", dummyContent) + .setManifest( + androidManifest( + "com.test.app", withMinSdkVersion(15), withMaxSdkVersion(27))) + .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .addModule( + "asset_module", + builder -> + builder + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery())) + .addFile("assets/textures/texture.etc", dummyContent)) + .build(); + Path bundlePath = createAndStoreBundle(appBundle); + + BuildApksCommand command = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setAapt2Command(aapt2Command) + .build(); + + Path apkSetFilePath = execute(command); + ZipFile apkSetFile = openZipFile(apkSetFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + // Standalone variant. + ImmutableList standaloneVariants = standaloneApkVariants(result); + assertThat(standaloneVariants).hasSize(1); + Variant standaloneVariant = standaloneVariants.get(0); + + assertThat(standaloneVariant.getApkSetList()).hasSize(1); + ApkSet standaloneApk = standaloneVariant.getApkSet(0); + assertThat(standaloneApk.getApkDescriptionList()).hasSize(1); + assertThat(apkSetFile).hasFile(standaloneApk.getApkDescription(0).getPath()); + + File standaloneApkFile = + extractFromApkSetFile(apkSetFile, standaloneApk.getApkDescription(0).getPath(), outputDir); + + try (ZipFile apkZip = new ZipFile(standaloneApkFile)) { + assertThat(apkZip).hasFile("classes.dex").thatIsCompressed(); + assertThat(apkZip).hasFile("assets/images/image.jpg").thatIsCompressed(); + assertThat(apkZip).hasFile("assets/textures/texture.etc").thatIsUncompressed(); + } + + // L+ assets. + assertThat(result.getAssetSliceSetCount()).isEqualTo(1); + AssetSliceSet assetSlice = result.getAssetSliceSet(0); + assertThat(assetSlice.getApkDescriptionCount()).isEqualTo(1); + + File apkFile = + extractFromApkSetFile(apkSetFile, assetSlice.getApkDescription(0).getPath(), outputDir); + + try (ZipFile apkZip = new ZipFile(apkFile)) { + assertThat(apkZip).hasFile("assets/textures/texture.etc").thatIsUncompressed(); + } + } + private static ImmutableList apkDescriptions(List variants) { return variants.stream() .flatMap(variant -> apkDescriptions(variant).stream()) @@ -4296,15 +4429,20 @@ private Path createAndStoreBundle(AppBundle appBundle) throws IOException { } private int extractVersionCode(File apk) { + return extractAndroidManifest(apk) + .getVersionCode() + .orElseThrow(VersionCodeMissingException::new); + } + + private AndroidManifest extractAndroidManifest(File apk) { Path protoApkPath = tmpDir.resolve("proto.apk"); Aapt2Helper.convertBinaryApkToProtoApk(apk.toPath(), protoApkPath); try { try (ZipFile protoApk = new ZipFile(protoApkPath.toFile())) { - AndroidManifest androidManifest = - AndroidManifest.create( - XmlNode.parseFrom( - protoApk.getInputStream(protoApk.getEntry("AndroidManifest.xml")))); - return androidManifest.getVersionCode().orElseThrow(VersionCodeMissingException::new); + return AndroidManifest.create( + XmlNode.parseFrom( + protoApk.getInputStream(protoApk.getEntry("AndroidManifest.xml")), + ExtensionRegistry.getEmptyRegistry())); } finally { Files.deleteIfExists(protoApkPath); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java index bd9df60c..03dc83b2 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -1354,6 +1354,358 @@ public void testExtractFromDirectoryNoTableOfContents_throws() throws Exception } + @Test + public void conditionalModule_deviceMatching() throws Exception { + ZipPath apkBase = ZipPath.create("apkL-base.apk"); + ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(sdkVersionFrom(1))), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkBase)), + createConditionalApkSet( + "conditional", + mergeModuleTargeting( + moduleMinSdkVersionTargeting(24), + moduleFeatureTargeting("android.hardware.camera.ar")), + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), apkConditional)))) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = + mergeSpecs(deviceWithSdk(24), deviceFeatures("android.hardware.camera.ar")); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly(inOutputDirectory(apkConditional), inOutputDirectory(apkBase)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void conditionalModule_deviceNotMatching() throws Exception { + ZipPath apkBase = ZipPath.create("apkL-base.apk"); + ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(sdkVersionFrom(1))), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkBase)), + createConditionalApkSet( + "conditional", + mergeModuleTargeting( + moduleMinSdkVersionTargeting(24), + moduleFeatureTargeting("android.hardware.camera.ar")), + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), apkConditional)))) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21)); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks).containsExactly(inOutputDirectory(apkBase)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void conditionalModule_deviceNotMatching_moduleInFlags() throws Exception { + ZipPath apkBase = ZipPath.create("apkL-base.apk"); + ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting(sdkVersionFrom(21), ImmutableSet.of(sdkVersionFrom(1))), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkBase)), + createConditionalApkSet( + "conditional", + mergeModuleTargeting( + moduleMinSdkVersionTargeting(24), + moduleFeatureTargeting("android.hardware.camera.ar")), + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), apkConditional)))) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21)); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .setModules(ImmutableSet.of("conditional")) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly(inOutputDirectory(apkConditional), inOutputDirectory(apkBase)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + /** Ensures that --modules=_ALL_ extracts all modules. */ + @Test + public void shortcutToExtractAllModules() throws Exception { + ZipPath apkBase = ZipPath.create("base-master.apk"); + ZipPath apkBaseXxhdpi = ZipPath.create("base-xxhdpi.apk"); + ZipPath apkFeature = ZipPath.create("feature-master.apk"); + ZipPath apkFeatureXxhdpi = ZipPath.create("feature-xxhdpi.apk"); + ZipPath apkFeature2 = ZipPath.create("feature2.apk"); + ZipPath apkFeature2Arm64 = ZipPath.create("feature2-arm64_v8a.apk"); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkBase), + createApkDescription(apkDensityTargeting(XXHDPI), apkBaseXxhdpi, false)), + createSplitApkSet( + "feature", + DeliveryType.ON_DEMAND, + /* moduleDependencies= */ ImmutableList.of("feature2"), + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkFeature), + createApkDescription(apkDensityTargeting(XXHDPI), apkFeatureXxhdpi, false)), + createSplitApkSet( + "feature2", + DeliveryType.ON_DEMAND, + /* moduleDependencies= */ ImmutableList.of(), + createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkFeature2), + createApkDescription(apkAbiTargeting(ARM64_V8A), apkFeature2Arm64, false)))) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = deviceWithSdk(21); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .setModules(ImmutableSet.of("_ALL_")) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly( + inOutputDirectory(apkBase), + inOutputDirectory(apkBaseXxhdpi), + inOutputDirectory(apkFeature), + inOutputDirectory(apkFeatureXxhdpi), + inOutputDirectory(apkFeature2), + inOutputDirectory(apkFeature2Arm64)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void extractAssetModules() throws Exception { + String installTimeModule1 = "installtime_assetmodule1"; + String installTimeModule2 = "installtime_assetmodule2"; + String onDemandModule = "ondemand_assetmodule"; + ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); + ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); + ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); + ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); + ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + ZipPath baseApk = ZipPath.create("base-master.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule1) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule2) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(onDemandModule) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) + .build(); + + Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = lDeviceWithLocales("en-US"); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly( + inOutputDirectory(installTimeMasterApk1), + inOutputDirectory(installTimeEnApk1), + inOutputDirectory(installTimeMasterApk2), + inOutputDirectory(installTimeEnApk2), + inOutputDirectory(baseApk)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void extractAssetModules_allModules() throws Exception { + String installTimeModule1 = "installtime_assetmodule1"; + String installTimeModule2 = "installtime_assetmodule2"; + String onDemandModule = "ondemand_assetmodule"; + ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); + ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); + ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); + ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); + ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + ZipPath baseApk = ZipPath.create("base-master.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule1) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule2) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(onDemandModule) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) + .build(); + + Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + + DeviceSpec deviceSpec = lDeviceWithLocales("en-US"); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .setModules(ImmutableSet.of("_ALL_")) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly( + inOutputDirectory(installTimeMasterApk1), + inOutputDirectory(installTimeEnApk1), + inOutputDirectory(installTimeMasterApk2), + inOutputDirectory(installTimeEnApk2), + inOutputDirectory(onDemandMasterApk), + inOutputDirectory(baseApk)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + private Path createApks(BuildApksResult buildApksResult, boolean apksInDirectory) throws Exception { if (apksInDirectory) { diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java index 55a8be72..eed6fbe1 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java @@ -38,6 +38,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,6 +46,7 @@ import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; +import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Config.Bundletool; import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.Abi.AbiAlias; @@ -65,7 +67,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -86,6 +87,7 @@ public class InstallApksCommandTest { private static final String DEVICE_ID = "id1"; + private static final String PKG_NAME = "com.example"; @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -220,7 +222,6 @@ public void fromFlagsEquivalentToBuilder_androidSerialVariable() throws Exceptio assertThat(fromBuilder).isEqualTo(fromFlags); } - @Test public void fromFlagsEquivalentToBuilder_modules() throws Exception { Path apksFile = tmpDir.resolve("appbundle.apks"); @@ -245,7 +246,6 @@ public void fromFlagsEquivalentToBuilder_modules() throws Exception { assertThat(fromBuilder).isEqualTo(fromFlags); } - @Test public void missingApksFlag_fails() { expectMissingRequiredBuilderPropertyException( @@ -567,7 +567,7 @@ public void installsOnlySpecifiedModules( .build() .execute(); - assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + assertThat(getFileNames(installedApks)) .containsExactly("base-master.apk", "feature1-master.apk", "feature2-master.apk"); } @@ -626,7 +626,7 @@ public void moduleDependencies_installDependency( .build() .execute(); - assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + assertThat(getFileNames(installedApks)) .containsExactly("base-master.apk", "feature1-master.apk", "feature2-master.apk"); } @@ -692,7 +692,7 @@ public void moduleDependencies_diamondGraph( .build() .execute(); - assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + assertThat(getFileNames(installedApks)) .containsExactly( "base-master.apk", "feature1-master.apk", @@ -703,8 +703,17 @@ public void moduleDependencies_diamondGraph( @Test @Theory - public void installModules_withPush(@FromDataPoints("apksInDirectory") boolean apksInDirectory) + public void installAssetModules(@FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + String installTimeModule1 = "installtime_assetmodule1"; + String installTimeModule2 = "installtime_assetmodule2"; + String onDemandModule = "ondemand_assetmodule"; + ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); + ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); + ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); + ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); + ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + ZipPath baseApk = ZipPath.create("base-master.apk"); BuildApksResult tableOfContent = BuildApksResult.newBuilder() .setBundletool( @@ -715,55 +724,81 @@ public void installModules_withPush(@FromDataPoints("apksInDirectory") boolean a VariantTargeting.getDefaultInstance(), createSplitApkSet( "base", - createMasterApkDescription( - ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk"))), - createSplitApkSet( - "base", - createApkDescription( - apkLanguageTargeting("pl"), - ZipPath.create("base-pl.apk"), - /* isMasterSplit= */ false)))) + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule1) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeModule2) + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + .addApkDescription( + splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(onDemandModule) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) .build(); + Path apksFile = createApks(tableOfContent, apksInDirectory); + List installedApks = new ArrayList<>(); - List pushedApks = new ArrayList<>(); FakeDevice fakeDevice = FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); AdbServer adbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); - fakeDevice.setPushApksSideEffect((apks, installOptions) -> pushedApks.addAll(apks)); InstallApksCommand.builder() .setApksArchivePath(apksFile) .setAdbPath(adbPath) .setAdbServer(adbServer) - .setPushSplitsPath("/tmp/") + .setModules(ImmutableSet.of(installTimeModule1)) .build() .execute(); - assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) - .containsExactly("base-master.apk"); - - assertThat(Lists.transform(pushedApks, apkPath -> apkPath.getFileName().toString())) - .containsExactly("base-master.apk", "base-pl.apk"); + assertThat(getFileNames(installedApks)) + .containsExactly( + baseApk.toString(), installTimeMasterApk1.toString(), installTimeEnApk1.toString()); } @Test @Theory - public void extractAssetModules(@FromDataPoints("apksInDirectory") boolean apksInDirectory) - throws Exception { - String installTimeModule1 = "installtime_assetmodule1"; - String installTimeModule2 = "installtime_assetmodule2"; - String onDemandModule = "ondemand_assetmodule"; - ZipPath installTimeMasterApk1 = ZipPath.create(installTimeModule1 + "-master.apk"); - ZipPath installTimeEnApk1 = ZipPath.create(installTimeModule1 + "-en.apk"); - ZipPath installTimeMasterApk2 = ZipPath.create(installTimeModule2 + "-master.apk"); - ZipPath installTimeEnApk2 = ZipPath.create(installTimeModule2 + "-en.apk"); - ZipPath onDemandMasterApk = ZipPath.create(onDemandModule + "-master.apk"); + public void localTestingMode_defaultModules( + @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + String installTimeFeature = "installtime_feature"; + String onDemandFeature = "ondemand_feature"; + String installTimeAsset = "installtime_asset"; + String onDemandAsset = "ondemand_asset"; ZipPath baseApk = ZipPath.create("base-master.apk"); + ZipPath baseEnApk = ZipPath.create("base-en.apk"); + ZipPath installTimeFeatureMasterApk = ZipPath.create(installTimeFeature + "-master.apk"); + ZipPath installTimeFeatureEnApk = ZipPath.create(installTimeFeature + "-en.apk"); + ZipPath installTimeFeaturePlApk = ZipPath.create(installTimeFeature + "-pl.apk"); + ZipPath onDemandFeatureMasterApk = ZipPath.create(onDemandFeature + "-master.apk"); + ZipPath installTimeAssetMasterApk = ZipPath.create(installTimeAsset + "-master.apk"); + ZipPath installTimeAssetEnApk = ZipPath.create(installTimeAsset + "-en.apk"); + ZipPath onDemandAssetMasterApk = ZipPath.create(onDemandAsset + "-master.apk"); BuildApksResult tableOfContent = BuildApksResult.newBuilder() + .setPackageName(PKG_NAME) .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) @@ -772,59 +807,206 @@ public void extractAssetModules(@FromDataPoints("apksInDirectory") boolean apksI VariantTargeting.getDefaultInstance(), createSplitApkSet( "base", - createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk)))) + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk), + createApkDescription( + apkLanguageTargeting("en"), baseEnApk, /* isMasterSplit= */ false)), + createSplitApkSet( + installTimeFeature, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), installTimeFeatureMasterApk), + createApkDescription( + apkLanguageTargeting("en"), + installTimeFeatureEnApk, + /* isMasterSplit= */ false), + createApkDescription( + apkLanguageTargeting("pl"), + installTimeFeaturePlApk, + /* isMasterSplit= */ false)), + createSplitApkSet( + onDemandFeature, + DeliveryType.ON_DEMAND, + /* moduleDependencies= */ ImmutableList.of(), + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), onDemandFeatureMasterApk)))) .addAssetSliceSet( AssetSliceSet.newBuilder() .setAssetModuleMetadata( AssetModuleMetadata.newBuilder() - .setName(installTimeModule1) + .setName(installTimeAsset) .setDeliveryType(DeliveryType.INSTALL_TIME)) .addApkDescription( splitApkDescription( - ApkTargeting.getDefaultInstance(), installTimeMasterApk1)) + ApkTargeting.getDefaultInstance(), installTimeAssetMasterApk)) .addApkDescription( - splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk1))) + splitApkDescription(apkLanguageTargeting("en"), installTimeAssetEnApk))) .addAssetSliceSet( AssetSliceSet.newBuilder() .setAssetModuleMetadata( AssetModuleMetadata.newBuilder() - .setName(installTimeModule2) + .setName(onDemandAsset) + .setDeliveryType(DeliveryType.ON_DEMAND)) + .addApkDescription( + splitApkDescription( + ApkTargeting.getDefaultInstance(), onDemandAssetMasterApk))) + .setLocalTestingInfo( + LocalTestingInfo.newBuilder() + .setEnabled(true) + .setLocalTestingPath("local_testing") + .build()) + .build(); + + Path apksFile = createApks(tableOfContent, apksInDirectory); + + List installedApks = new ArrayList<>(); + List pushedApks = new ArrayList<>(); + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); + AdbServer adbServer = + new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); + fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + fakeDevice.setPushApksSideEffect((apks, installOptions) -> pushedApks.addAll(apks)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .build() + .execute(); + + // Base, install-time features and install-time assets. + assertThat(getFileNames(installedApks)) + .containsExactly( + baseApk.toString(), + baseEnApk.toString(), + installTimeFeatureMasterApk.toString(), + installTimeFeatureEnApk.toString(), + installTimeAssetMasterApk.toString(), + installTimeAssetEnApk.toString()); + // Base config splits, install-time and on-demand features and on-demand assets. All languages. + assertThat(getFileNames(pushedApks)) + .containsExactly( + baseEnApk.toString(), + installTimeFeatureMasterApk.toString(), + installTimeFeatureEnApk.toString(), + installTimeFeaturePlApk.toString(), + onDemandFeatureMasterApk.toString(), + onDemandAssetMasterApk.toString()); + } + + @Test + @Theory + public void localTestingMode_allModules( + @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + String installTimeFeature = "installtime_feature"; + String onDemandFeature = "ondemand_feature"; + String installTimeAsset = "installtime_asset"; + String onDemandAsset = "ondemand_asset"; + ZipPath baseApk = ZipPath.create("base-master.apk"); + ZipPath baseEnApk = ZipPath.create("base-en.apk"); + ZipPath installTimeFeatureMasterApk = ZipPath.create(installTimeFeature + "-master.apk"); + ZipPath installTimeFeatureEnApk = ZipPath.create(installTimeFeature + "-en.apk"); + ZipPath installTimeFeaturePlApk = ZipPath.create(installTimeFeature + "-pl.apk"); + ZipPath onDemandFeatureMasterApk = ZipPath.create(onDemandFeature + "-master.apk"); + ZipPath installTimeAssetMasterApk = ZipPath.create(installTimeAsset + "-master.apk"); + ZipPath installTimeAssetEnApk = ZipPath.create(installTimeAsset + "-en.apk"); + ZipPath onDemandAssetMasterApk = ZipPath.create(onDemandAsset + "-master.apk"); + BuildApksResult tableOfContent = + BuildApksResult.newBuilder() + .setPackageName(PKG_NAME) + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + VariantTargeting.getDefaultInstance(), + createSplitApkSet( + "base", + createMasterApkDescription(ApkTargeting.getDefaultInstance(), baseApk), + createApkDescription( + apkLanguageTargeting("en"), baseEnApk, /* isMasterSplit= */ false)), + createSplitApkSet( + installTimeFeature, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), installTimeFeatureMasterApk), + createApkDescription( + apkLanguageTargeting("en"), + installTimeFeatureEnApk, + /* isMasterSplit= */ false), + createApkDescription( + apkLanguageTargeting("pl"), + installTimeFeaturePlApk, + /* isMasterSplit= */ false)), + createSplitApkSet( + onDemandFeature, + DeliveryType.ON_DEMAND, + /* moduleDependencies= */ ImmutableList.of(), + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), onDemandFeatureMasterApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName(installTimeAsset) .setDeliveryType(DeliveryType.INSTALL_TIME)) .addApkDescription( splitApkDescription( - ApkTargeting.getDefaultInstance(), installTimeMasterApk2)) + ApkTargeting.getDefaultInstance(), installTimeAssetMasterApk)) .addApkDescription( - splitApkDescription(apkLanguageTargeting("en"), installTimeEnApk2))) + splitApkDescription(apkLanguageTargeting("en"), installTimeAssetEnApk))) .addAssetSliceSet( AssetSliceSet.newBuilder() .setAssetModuleMetadata( AssetModuleMetadata.newBuilder() - .setName(onDemandModule) + .setName(onDemandAsset) .setDeliveryType(DeliveryType.ON_DEMAND)) .addApkDescription( - splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) + splitApkDescription( + ApkTargeting.getDefaultInstance(), onDemandAssetMasterApk))) + .setLocalTestingInfo( + LocalTestingInfo.newBuilder() + .setEnabled(true) + .setLocalTestingPath("local_testing") + .build()) .build(); Path apksFile = createApks(tableOfContent, apksInDirectory); List installedApks = new ArrayList<>(); + List pushedApks = new ArrayList<>(); FakeDevice fakeDevice = FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); AdbServer adbServer = new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + fakeDevice.setPushApksSideEffect((apks, installOptions) -> pushedApks.addAll(apks)); InstallApksCommand.builder() .setApksArchivePath(apksFile) .setAdbPath(adbPath) .setAdbServer(adbServer) - .setModules(ImmutableSet.of(installTimeModule1)) + .setModules(ImmutableSet.of("_ALL_")) .build() .execute(); - assertThat(Lists.transform(installedApks, apkPath -> apkPath.getFileName().toString())) + // Base, install-time and on-demand features and install-time assets. + assertThat(getFileNames(installedApks)) .containsExactly( - baseApk.toString(), installTimeMasterApk1.toString(), installTimeEnApk1.toString()); + baseApk.toString(), + baseEnApk.toString(), + installTimeFeatureMasterApk.toString(), + installTimeFeatureEnApk.toString(), + onDemandFeatureMasterApk.toString(), + installTimeAssetMasterApk.toString(), + installTimeAssetEnApk.toString()); + // Base config splits, install-time and on-demand features and on-demand assets. All languages. + assertThat(getFileNames(pushedApks)) + .containsExactly( + baseEnApk.toString(), + installTimeFeatureMasterApk.toString(), + installTimeFeatureEnApk.toString(), + installTimeFeaturePlApk.toString(), + onDemandFeatureMasterApk.toString(), + onDemandAssetMasterApk.toString()); } @Test @@ -870,4 +1052,8 @@ private Path createApks(BuildApksResult buildApksResult, boolean apksInDirectory return createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); } } + + private static ImmutableList getFileNames(List paths) { + return paths.stream().map(Path::getFileName).map(Path::toString).collect(toImmutableList()); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java b/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java index a9fae179..3c36d283 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/AdbRunnerTest.java @@ -27,9 +27,14 @@ import com.android.tools.build.bundletool.testing.FakeAdbServer; import com.android.tools.build.bundletool.testing.FakeDevice; import com.google.common.collect.ImmutableList; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -43,6 +48,15 @@ public class AdbRunnerTest { .setTimeout(Duration.ofMillis(100)) .build(); + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + private Path apkPath; + + @Before + public void setUp() throws Exception { + apkPath = Paths.get(tmp.getRoot().toString(), "apkOne.apk"); + Files.createFile(apkPath); + } + @Test public void installApks_noDeviceId_noConnectedDevices_throws() { AdbServer testAdbServer = @@ -56,8 +70,7 @@ public void installApks_noDeviceId_noConnectedDevices_throws() { () -> adbRunner.run( device -> - device.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS))); + device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS))); assertThat(exception) .hasMessageThat() .contains("Expected to find one connected device, but found none."); @@ -73,9 +86,7 @@ public void installApks_noDeviceId_oneConnectedDevice_ok() { testAdbServer.init(Paths.get("/test/adb")); AdbRunner adbRunner = new AdbRunner(testAdbServer); - adbRunner.run( - device -> - device.installApks(ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS)); + adbRunner.run(device -> device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS)); } @Test @@ -95,8 +106,7 @@ public void installApks_noDeviceId_twoConnectedDevices_throws() { () -> adbRunner.run( device -> - device.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS))); + device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS))); assertThat(exception) .hasMessageThat() .contains("Expected to find one connected device, but found 2."); @@ -115,8 +125,7 @@ public void installApks_withDeviceId_noConnectedDevices_throws() { () -> adbRunner.run( device -> - device.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS), "device1")); assertThat(exception) .hasMessageThat() @@ -136,8 +145,7 @@ public void installApks_withDeviceId_connectedDevices_ok() { AdbRunner adbRunner = new AdbRunner(testAdbServer); adbRunner.run( - device -> - device.installApks(ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + device -> device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS), "device1"); } @@ -167,8 +175,7 @@ public void installApks_withDeviceId_disconnectedDevice_throws() { () -> adbRunner.run( device -> - device.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), DEFAULT_INSTALL_OPTIONS), + device.installApks(ImmutableList.of(apkPath), DEFAULT_INSTALL_OPTIONS), "device2")); assertThat(exception) .hasMessageThat() @@ -194,7 +201,7 @@ public void installApks_allowingDowngrade() { adbRunner.run( device -> device.installApks( - ImmutableList.of(Paths.get("apkOne.apk")), + ImmutableList.of(apkPath), InstallOptions.builder().setAllowDowngrade(true).build())); } } 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 483bdc23..27156d47 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java @@ -1503,9 +1503,12 @@ public void assetModuleMatch() { .containsExactly(baseApk, installTimeMasterApk1, installTimeMasterApk2); assertThat( - new ApkMatcher(enDevice, Optional.of(ImmutableSet.of(installTimeModule1)), false) + new ApkMatcher( + enDevice, + Optional.of(ImmutableSet.of(installTimeModule1, onDemandModule)), + false) .getMatchingApks(buildApksResult)) - .containsExactly(baseApk, installTimeMasterApk1, installTimeEnApk1); + .containsExactly(baseApk, installTimeMasterApk1, installTimeEnApk1, onDemandMasterApk); } private static BuildApksResult buildApksResult(Variant... variants) { diff --git a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java index 2754b04d..e8a03cf4 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java @@ -19,7 +19,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; 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 c3848a6c..a4c54b0d 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/ModuleSplitsToShardMergerTest.java @@ -52,7 +52,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; 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 7b0e0f8b..937124b5 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AppBundleTest.java @@ -429,8 +429,8 @@ public void baseAndAssetModule_fromZipFile_areSeparated() throws Exception { .addFileWithContent(ZipPath.create("base/dex/classes.dex"), DUMMY_CONTENT) .addFileWithContent(ZipPath.create("base/assets/file.txt"), DUMMY_CONTENT) .addFileWithProtoContent( - ZipPath.create("remote_assets/manifest/AndroidManifest.xml"), ASSET_MODULE_MANIFEST) - .addFileWithContent(ZipPath.create("remote_assets/assets/file.txt"), DUMMY_CONTENT) + ZipPath.create("asset_module/manifest/AndroidManifest.xml"), ASSET_MODULE_MANIFEST) + .addFileWithContent(ZipPath.create("asset_module/assets/file.txt"), DUMMY_CONTENT) .writeTo(bundleFile); try (ZipFile appBundleZip = new ZipFile(bundleFile.toFile())) { @@ -438,7 +438,7 @@ public void baseAndAssetModule_fromZipFile_areSeparated() throws Exception { assertThat(appBundle.getFeatureModules().keySet()) .containsExactly(BundleModuleName.create("base")); assertThat(appBundle.getAssetModules().keySet()) - .containsExactly(BundleModuleName.create("remote_assets")); + .containsExactly(BundleModuleName.create("asset_module")); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java index 7bc1365d..7cd31caf 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -20,6 +20,10 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_CERT_SHA256_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_TYPE_METADATA_KEY; +import static com.android.tools.build.bundletool.testing.CertificateFactory.buildSelfSignedCertificate; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMainActivity; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitNameActivity; @@ -59,12 +63,19 @@ 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.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.utils.CertificateHelper; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Arrays; +import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -77,13 +88,27 @@ public class ModuleSplitTest { private static final int VERSION_CODE_RESOURCE_ID = 0x0101021b; + private static SigningConfiguration stampSigningConfig; + private static String stampCertSha256; + + @BeforeClass + public static void setUpClass() throws Exception { + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").genKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + X509Certificate certificate = buildSelfSignedCertificate(keyPair, "CN=ModuleSplitTest"); + stampSigningConfig = + SigningConfiguration.builder() + .setPrivateKey(privateKey) + .setCertificates(ImmutableList.of(certificate)) + .build(); + stampCertSha256 = CertificateHelper.sha256AsHexString(certificate); + } + @Test public void notPossibleToTargetMultipleDimensions() { - String fakeAssetPath = "testModule/assets/secret.txt"; ModuleSplit.Builder builder = ModuleSplit.builder() .setModuleName(BundleModuleName.create("testModule")) - .setEntries(ImmutableList.of(InMemoryModuleEntry.ofFile(fakeAssetPath, DUMMY_CONTENT))) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( ApkTargeting.newBuilder() @@ -102,12 +127,6 @@ public void testMasterSplitIdEqualsToModuleName_Base() { ModuleSplit masterSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries( - fakeEntriesOf( - "res/drawable-ldpi/image.jpg", - "assets/labels.dat", - "res/drawable-hdpi/image.jpg", - "dex/classes.dex")) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setVariantTargeting(lPlusVariantTargeting()) .setMasterSplit(true) @@ -122,12 +141,6 @@ public void testMasterSplitIdEqualsToModuleName_nonBase() { ModuleSplit masterSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("moduleA")) - .setEntries( - fakeEntriesOf( - "res/drawable-ldpi/image.jpg", - "assets/labels.dat", - "res/drawable-hdpi/image.jpg", - "dex/classes.dex")) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setVariantTargeting(lPlusVariantTargeting()) .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) @@ -193,35 +206,11 @@ public void graphicsApiTargetingIsReflectedInManifest() throws Exception { assertManifestHasGlTargeting(configManifest, 0x20000); } - @Ignore("Texture compression format is currently not being reflected in the manifest.") - @Test - public void textureCompressionFormatTargetingIsReflectedInManifest() throws Exception { - BundleModule module = - new BundleModuleBuilder("testModule") - .addFile("assets/dict.dat") - .setManifest(androidManifest("com.test.app")) - .build(); - ModuleSplit split = - ModuleSplit.forModule(module) - .toBuilder() - .setApkTargeting( - apkTextureTargeting( - textureCompressionTargeting(TextureCompressionFormatAlias.ETC1_RGB8))) - .setMasterSplit(false) - .build(); - - split = split.writeSplitIdInManifest(split.getSuffix()); - - XmlProtoNode configManifest = split.getAndroidManifest().getManifestRoot(); - assertManifestHasSingleGlTexture(configManifest, "GL_OES_compressed_ETC1_RGB8_texture"); - } - @Test public void moduleResourceSplitSuffixAndName() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting(apkDensityTargeting(DensityAlias.HDPI)) .setMasterSplit(false) @@ -236,7 +225,6 @@ public void moduleOpenGlSplitSuffixAndName() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( apkGraphicsTargeting(graphicsApiTargeting(openGlVersionFrom(3, 1)))) // 3.1 @@ -252,7 +240,6 @@ public void moduleVulkanSplitSuffixAndName() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( apkGraphicsTargeting(graphicsApiTargeting(vulkanVersionFrom(3, 1)))) // 3.1 @@ -268,7 +255,6 @@ public void moduleOpenGlSplitSuffixAndName_alternatives() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( apkGraphicsTargeting( @@ -289,7 +275,6 @@ public void moduleTextureSplitSuffixAndName_alternatives() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( apkTextureTargeting( @@ -308,7 +293,6 @@ public void moduleAbiSplitSuffixAndName_alternatives() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting( apkAbiTargeting( @@ -330,7 +314,6 @@ public void apexModuleMultiAbiSplitSuffixAndName() { ModuleSplit resSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(VariantTargeting.getDefaultInstance()) .setApkTargeting( apkMultiAbiTargeting( @@ -352,7 +335,6 @@ public void moduleLanguageSplitSuffixAndName() { ModuleSplit langSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting(apkLanguageTargeting(languageTargeting("es"))) .setMasterSplit(false) @@ -367,7 +349,6 @@ public void moduleLanguageSplitFallback_suffixAndName() { ModuleSplit langSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting(apkLanguageTargeting(alternativeLanguageTargeting("es"))) .setMasterSplit(false) @@ -382,7 +363,6 @@ public void moduleSanitizerSplitSuffixAndName() { ModuleSplit sanitizerSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries(ImmutableList.of()) .setVariantTargeting(lPlusVariantTargeting()) .setApkTargeting(apkSanitizerTargeting(SanitizerAlias.HWADDRESS)) .setMasterSplit(false) @@ -403,12 +383,6 @@ public void splitNameRemoved() { ModuleSplit masterSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries( - fakeEntriesOf( - "res/drawable-ldpi/image.jpg", - "assets/labels.dat", - "res/drawable-hdpi/image.jpg", - "dex/classes.dex")) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setVariantTargeting(lPlusVariantTargeting()) .setMasterSplit(true) @@ -442,12 +416,6 @@ public void removeUnknownSplits() { ModuleSplit masterSplit = ModuleSplit.builder() .setModuleName(BundleModuleName.create("base")) - .setEntries( - fakeEntriesOf( - "res/drawable-ldpi/image.jpg", - "assets/labels.dat", - "res/drawable-hdpi/image.jpg", - "dex/classes.dex")) .setApkTargeting(ApkTargeting.getDefaultInstance()) .setVariantTargeting(lPlusVariantTargeting()) .setMasterSplit(true) @@ -470,20 +438,65 @@ public void removeUnknownSplits() { xmlAttribute(ANDROID_NAMESPACE_URI, "name", NAME_RESOURCE_ID, "MainActivity")); } + @Test + public void testMasterBaseSplit_containsStamp() throws Exception { + ModuleSplit masterSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(lPlusVariantTargeting()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .setMasterSplit(true) + .build(); + SourceStamp sourceStamp = + SourceStamp.builder() + .setSource("test-source") + .setSigningConfiguration(stampSigningConfig) + .build(); + StampType stampType = StampType.STAMP_TYPE_DEFAULT; + + masterSplit = masterSplit.writeStampInManifest(sourceStamp, stampType); + + assertThat(masterSplit.getAndroidManifest().getMetadataValue(STAMP_TYPE_METADATA_KEY)) + .hasValue(stampType.toString()); + assertThat(masterSplit.getAndroidManifest().getMetadataValue(STAMP_SOURCE_METADATA_KEY)) + .hasValue(sourceStamp.getSource()); + assertThat(masterSplit.getAndroidManifest().getMetadataValue(STAMP_CERT_SHA256_METADATA_KEY)) + .hasValue(stampCertSha256); + } + + @Test + public void testNonMasterBaseSplit_doesNotContainStamp() throws Exception { + ModuleSplit abiSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .setVariantTargeting(lPlusVariantTargeting()) + .setApkTargeting( + apkAbiTargeting(ImmutableSet.of(AbiAlias.X86), ImmutableSet.of(AbiAlias.X86))) + .setMasterSplit(false) + .build(); + SourceStamp sourceStamp = + SourceStamp.builder() + .setSource("test-source") + .setSigningConfiguration(stampSigningConfig) + .build(); + StampType stampType = StampType.STAMP_TYPE_DEFAULT; + + abiSplit = abiSplit.writeStampInManifest(sourceStamp, stampType); + + assertThat(abiSplit.getAndroidManifest().getMetadataValue(STAMP_TYPE_METADATA_KEY)).isEmpty(); + assertThat(abiSplit.getAndroidManifest().getMetadataValue(STAMP_SOURCE_METADATA_KEY)).isEmpty(); + assertThat(abiSplit.getAndroidManifest().getMetadataValue(STAMP_CERT_SHA256_METADATA_KEY)) + .isEmpty(); + } + private ImmutableList fakeEntriesOf(String... entries) { return Arrays.stream(entries) .map(entry -> InMemoryModuleEntry.ofFile(entry, DUMMY_CONTENT)) .collect(toImmutableList()); } - private static void assertManifestHasSingleGlTexture( - XmlProtoNode manifest, String textureString) { - XmlElement glTexture = manifest.getElement().getChildElement("supports-gl-texture").getProto(); - assertThat(glTexture.getAttributeList()).hasSize(1); - assertThat(glTexture.getAttribute(0)) - .isEqualTo(xmlAttribute(ANDROID_NAMESPACE_URI, "name", textureString)); - } - private static void assertManifestHasGlTargeting(XmlProtoNode manifest, int expectedValue) { ImmutableList features = manifest 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 ec87f35f..d318e793 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java @@ -1445,7 +1445,68 @@ public void wholeResourcePinning_allConfigsInMaster() throws Exception { BUNDLETOOL_VERSION, ApkGenerationConfiguration.builder() .setOptimizationDimensions(ImmutableSet.of(LANGUAGE)) - .setMasterPinnedResources(ImmutableSet.of(ResourceId.create(0x7f010001))) + .setMasterPinnedResourceIds(ImmutableSet.of(ResourceId.create(0x7f010001))) + .build(), + lPlusVariantTargeting(), + ImmutableSet.of("base")); + + ImmutableList splits = moduleSplitter.splitModule(); + + Map splitsBySuffix = Maps.uniqueIndex(splits, ModuleSplit::getSuffix); + assertThat(splitsBySuffix.keySet()).containsExactly("", "de", "ru"); + + assertThat(splitsBySuffix.get("").getResourceTable().get()) + .containsResource("com.test.app:string/welcome_label") + .onlyWithConfigs(Configuration.getDefaultInstance(), locale("de"), locale("ru")); + assertThat(splitsBySuffix.get("de").getResourceTable().get()) + .doesNotContainResource("com.test.app:string/welcome_label"); + assertThat(splitsBySuffix.get("ru").getResourceTable().get()) + .doesNotContainResource("com.test.app:string/welcome_label"); + + assertThat(splitsBySuffix.get("").getResourceTable().get()) + .containsResource("com.test.app:string/goodbye_label") + .onlyWithConfigs(Configuration.getDefaultInstance()); + assertThat(splitsBySuffix.get("de").getResourceTable().get()) + .containsResource("com.test.app:string/goodbye_label") + .onlyWithConfigs(locale("de")); + assertThat(splitsBySuffix.get("ru").getResourceTable().get()) + .containsResource("com.test.app:string/goodbye_label") + .onlyWithConfigs(locale("ru")); + } + + @Test + public void wholeResourcePinning_langResourcePinnedByName() throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "string", + entry( + 0x0001, + "welcome_label", + value("Welcome", Configuration.getDefaultInstance()), + value("Willkommen", locale("de")), + value("Здравствуйте", locale("ru"))), + entry( + 0x0002, + "goodbye_label", + value("Goodbye", Configuration.getDefaultInstance()), + value("Auf Wiedersehen", locale("de")), + value("До свидания", locale("ru"))))))) + .setManifest(androidManifest("com.test.app")) + .build(); + ModuleSplitter moduleSplitter = + new ModuleSplitter( + baseModule, + BUNDLETOOL_VERSION, + ApkGenerationConfiguration.builder() + .setOptimizationDimensions(ImmutableSet.of(LANGUAGE)) + .setMasterPinnedResourceNames(ImmutableSet.of("welcome_label")) .build(), lPlusVariantTargeting(), ImmutableSet.of("base")); diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java index 8e496f14..1e3ee95f 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Locale; @@ -228,11 +229,17 @@ public void executeShellCommand( @Override public void installApks(ImmutableList apks, InstallOptions installOptions) { + for (Path apk : apks) { + checkState(Files.exists(apk)); + } installApksSideEffect.ifPresent(val -> val.apply(apks, installOptions)); } @Override public void pushApks(ImmutableList apks, PushOptions pushOptions) { + for (Path apk : apks) { + checkState(Files.exists(apk)); + } pushApksSideEffect.ifPresent(val -> val.apply(apks, pushOptions)); } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java new file mode 100644 index 00000000..a8ca9fa3 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java @@ -0,0 +1,193 @@ +/* + * 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.validation; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.ValidationException; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TextureCompressionFormatParityValidatorTest { + + @Test + public void noTCFs_ok() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a").setManifest(androidManifest("com.test.app")).build(); + BundleModule moduleB = + new BundleModuleBuilder("b").setManifest(androidManifest("com.test.app")).build(); + + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void sameTCFs_ok() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures#tcf_etc2/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .addFile("assets/other_textures#tcf_etc2/etc2_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void sameTCFsIncludingFallback_ok() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures#tcf_etc2/level1.assets") + .addFile("assets/textures/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .addFile("assets/other_textures#tcf_etc2/etc2_file.assets") + .addFile("assets/other_textures/fallback_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void sameTCFsAndNoTCF_ok() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures#tcf_etc2/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .addFile("assets/other_textures#tcf_etc2/etc2_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleC = + new BundleModuleBuilder("c") + .addFile("assets/untargeted_textures/level3.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB, moduleC)); + } + + @Test + public void sameTCFsIncludingFallbackAndNoTCF_ok() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures#tcf_etc2/level1.assets") + .addFile("assets/textures/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .addFile("assets/other_textures#tcf_etc2/etc2_file.assets") + .addFile("assets/other_textures/fallback_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleC = + new BundleModuleBuilder("c") + .addFile("assets/untargeted_textures/level3.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB, moduleC)); + } + + @Test + public void differentTCFs_throws() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures#tcf_etc2/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .addFile("assets/other_textures#tcf_pvrtc/etc2_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + ValidationException exception = + assertThrows( + ValidationException.class, + () -> + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB))); + + assertThat(exception) + .hasMessageThat() + .contains( + "All modules with targeted textures must have the same set of texture formats, but" + + " module 'a' has formats [ASTC, ETC2] (without fallback directories) and module" + + " 'b' has formats [ASTC, PVRTC] (without fallback directories)."); + } + + @Test + public void differentFallbacks_throws() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/textures#tcf_astc/level1.assets") + .addFile("assets/textures/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/other_textures#tcf_astc/astc_file.assets") + .setManifest(androidManifest("com.test.app")) + .build(); + + ValidationException exception = + assertThrows( + ValidationException.class, + () -> + new TextureCompressionFormatParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB))); + + assertThat(exception) + .hasMessageThat() + .contains( + "All modules with targeted textures must have the same set of texture formats, but" + + " module 'a' has formats [ASTC] (with fallback directories) and module 'b' has" + + " formats [ASTC] (without fallback directories)."); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java index 94a4cae8..37c7b29e 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/ValidatorRunnerTest.java @@ -21,7 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify;