diff --git a/README.md b/README.md index fce6e5e2..ddc51bba 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.13.1](https://github.com/google/bundletool/releases) +Latest release: [1.13.2](https://github.com/google/bundletool/releases) diff --git a/build.gradle b/build.gradle index 2bf8f40b..3776af93 100644 --- a/build.gradle +++ b/build.gradle @@ -201,6 +201,7 @@ shadowJar { exclude 'com.android.vending.splits' exclude 'com.android.vending.splits.required' exclude 'com.android.dynamic.apk.fused.modules' + exclude 'com.android.tools.build.profiles' } // Exclude ddmlib protos from maven jar diff --git a/dex_src/README.md b/dex_src/README.md deleted file mode 100644 index 27aaba46..00000000 --- a/dex_src/README.md +++ /dev/null @@ -1 +0,0 @@ -Contains the source-code for any .dex files in bundletool. diff --git a/gradle.properties b/gradle.properties index f9611cce..d97657e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.13.1 +release_version = 1.13.2 diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index a8e16702..5dc7ead8 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 @@ -33,12 +33,15 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; import com.android.apksig.SigningCertificateLineage; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.RuntimeEnabledSdkConfigProto.LocalDeploymentRuntimeEnabledSdkConfig; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.tools.build.bundletool.androidtools.Aapt2Command; import com.android.tools.build.bundletool.androidtools.P7ZipCommand; @@ -67,6 +70,7 @@ import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.files.BufferedIo; import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.android.tools.build.bundletool.preprocessors.AppBundlePreprocessorManager; import com.android.tools.build.bundletool.preprocessors.DaggerAppBundlePreprocessorComponent; @@ -88,21 +92,21 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.util.JsonFormat; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.RandomAccessFile; +import java.io.Reader; import java.io.UncheckedIOException; import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collection; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Executors; import java.util.logging.Logger; -import java.util.stream.Collectors; import java.util.zip.ZipException; import java.util.zip.ZipFile; @@ -211,6 +215,8 @@ public enum OutputFormat { Flag.pathSet("sdk-bundles"); private static final Flag> RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG = Flag.pathSet("sdk-archives"); + private static final Flag LOCAL_DEPLOYMENT_RUNTIME_ENABLED_SDK_CONFIG_FLAG = + Flag.path("local-runtime-enabled-sdk-config"); // Archive APK related flags. private static final Flag APP_STORE_PACKAGE_NAME_FLAG = Flag.string("store-package"); @@ -300,6 +306,9 @@ ListeningExecutorService getExecutorService() { public abstract ImmutableSet getRuntimeEnabledSdkArchivePaths(); + public abstract Optional + getLocalDeploymentRuntimeEnabledSdkConfig(); + public abstract Optional getAppStorePackageName(); public static Builder builder() { @@ -547,6 +556,13 @@ public Builder setCreateApkSetArchive(boolean createApkSetArchive) { */ public abstract Builder setRuntimeEnabledSdkArchivePaths(ImmutableSet sdkArchivePaths); + /** + * Options for overriding parts of runtime-enabled SDK dependency configuration of the app for + * local deployment and testing. + */ + public abstract Builder setLocalDeploymentRuntimeEnabledSdkConfig( + LocalDeploymentRuntimeEnabledSdkConfig localDeploymentRuntimeEnabledSdkConfig); + /** * Sets package name of an app store that will be called by archived app to redownload the * application. @@ -709,6 +725,16 @@ public BuildApksCommand build() { + " archives, but both were set.") .build(); } + + if (command.getLocalDeploymentRuntimeEnabledSdkConfig().isPresent() + && command.getRuntimeEnabledSdkBundlePaths().isEmpty() + && command.getRuntimeEnabledSdkArchivePaths().isEmpty()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Using --local-deployment-runtime-enabled-sdk-config flag requires either" + + " --sdk-bundles or --sdk-archives flag to be also present.") + .build(); + } return command; } } @@ -823,6 +849,25 @@ static BuildApksCommand fromFlags( .getValue(flags) .ifPresent(buildApksCommand::setRuntimeEnabledSdkArchivePaths); + if (LOCAL_DEPLOYMENT_RUNTIME_ENABLED_SDK_CONFIG_FLAG.getValue(flags).isPresent() + && !RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getValue(flags).isPresent() + && !RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getValue(flags).isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "'%s' flag can only be set together with '%s' or '%s' flags.", + LOCAL_DEPLOYMENT_RUNTIME_ENABLED_SDK_CONFIG_FLAG.getName(), + RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName(), + RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getName()) + .build(); + } + + LOCAL_DEPLOYMENT_RUNTIME_ENABLED_SDK_CONFIG_FLAG + .getValue(flags) + .ifPresent( + configPath -> + buildApksCommand.setLocalDeploymentRuntimeEnabledSdkConfig( + parseLocalRuntimeEnabledSdkConfig(configPath))); + APP_STORE_PACKAGE_NAME_FLAG.getValue(flags).ifPresent(buildApksCommand::setAppStorePackageName); flags.checkNoUnknownFlags(); @@ -914,6 +959,17 @@ private void validateInput() { checkFileExistsAndExecutable(getAdbPath().get()); } + if (!shouldGenerateSdkRuntimeVariant(getApkBuildMode())) { + checkArgument( + getRuntimeEnabledSdkBundlePaths().isEmpty(), + "runtimeEnabledSdkBundlePaths can not be present in '%s' mode.", + getApkBuildMode().name()); + checkArgument( + getRuntimeEnabledSdkArchivePaths().isEmpty(), + "runtimeEnabledSdkArchivePaths can not be present in '%s' mode.", + getApkBuildMode().name()); + } + getRuntimeEnabledSdkBundlePaths() .forEach( path -> { @@ -930,6 +986,9 @@ private void validateInput() { private ImmutableMap getValidatedSdkModules( Closer closer, TempDirectory tempDir, AppBundle appBundle) throws IOException { + if (!shouldGenerateSdkRuntimeVariant(getApkBuildMode())) { + return ImmutableMap.of(); + } if (!getRuntimeEnabledSdkArchivePaths().isEmpty()) { ImmutableMap sdkAsars = getValidatedSdkAsarsByPackageName(closer, tempDir); validateSdkAsarsMatchAppBundleDependencies(appBundle, sdkAsars); @@ -1579,6 +1638,18 @@ public static CommandHelp help() { + " .asar. Can not be used together with the '%s' flag.", RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName()) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(LOCAL_DEPLOYMENT_RUNTIME_ENABLED_SDK_CONFIG_FLAG.getName()) + .setExampleValue("path/to/config.json") + .setOptional(true) + .setDescription( + "Path to config file that contains options for overriding parts of" + + " runtime-enabled SDK dependency configuration of the app bundle. Can" + + " only be used if either '%s' or '%s' is set.", + RUNTIME_ENABLED_SDK_BUNDLE_LOCATIONS_FLAG.getName(), + RUNTIME_ENABLED_SDK_ARCHIVE_LOCATIONS_FLAG.getName()) + .build()) .addFlag( FlagDescription.builder() .setFlagName(APP_STORE_PACKAGE_NAME_FLAG.getName()) @@ -1593,10 +1664,7 @@ public static CommandHelp help() { } private static String joinFlagOptions(Enum... flagOptions) { - return Arrays.stream(flagOptions) - .map(Enum::name) - .map(String::toLowerCase) - .collect(Collectors.joining("|")); + return stream(flagOptions).map(Enum::name).map(String::toLowerCase).collect(joining("|")); } private static void populateSigningConfigurationFromFlags( @@ -1787,4 +1855,30 @@ private static SigningConfiguration getStampSigningConfiguration( return SigningConfiguration.extractFromKeystore( keystorePath, keyAlias, keystorePassword, keyPassword); } + + private static LocalDeploymentRuntimeEnabledSdkConfig parseLocalRuntimeEnabledSdkConfig( + Path configPath) { + try (Reader configReader = BufferedIo.reader(configPath)) { + LocalDeploymentRuntimeEnabledSdkConfig.Builder configBuilder = + LocalDeploymentRuntimeEnabledSdkConfig.newBuilder(); + JsonFormat.parser().merge(configReader, configBuilder); + return configBuilder.build(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static boolean shouldGenerateSdkRuntimeVariant(ApkBuildMode mode) { + switch (mode) { + case DEFAULT: + case UNIVERSAL: + case SYSTEM: + case PERSISTENT: + return true; + case INSTANT: + case ARCHIVE: + return false; + } + throw new IllegalStateException(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java index 610715cc..67722ed6 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java @@ -20,6 +20,7 @@ import com.android.bundle.Config.BundleConfig; import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.RuntimeEnabledSdkConfigProto.LocalDeploymentRuntimeEnabledSdkConfig; import com.android.tools.build.bundletool.androidtools.P7ZipCommand; import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.device.AdbServer; @@ -173,6 +174,13 @@ static boolean provideVerbose(BuildApksCommand command) { return command.getVerbose(); } + @CommandScoped + @Provides + static Optional provideLocalRuntimeEnabledSdkConfig( + BuildApksCommand command) { + return command.getLocalDeploymentRuntimeEnabledSdkConfig(); + } + /** * Qualifying annotation of an {@code Optional} for the first variant number to use when * numbering the generated variants. diff --git a/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java index 5887e1db..5bca3e50 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java @@ -124,7 +124,7 @@ protected ImmutableList getMatchingApks( countrySetTargeting, sdkRuntimeTargeting), getSizeRequest.getModules(), - /* includeInstallTimeAssetModules= */ true, + /* includeInstallTimeAssetModules= */ !getSizeRequest.getModules().isPresent(), getSizeRequest.getInstant(), /* ensureDensityAndAbiApksMatched= */ false) .getMatchingApksFromAssetModules(assetModules); diff --git a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java index d5caca93..54dbb19c 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java @@ -92,7 +92,7 @@ protected ImmutableList getMatchingApks( countrySetTargeting, sdkRuntimeTargeting), getSizeRequest.getModules(), - /* includeInstallTimeAssetModules= */ true, + /* includeInstallTimeAssetModules= */ false, getSizeRequest.getInstant(), /* ensureDensityAndAbiApksMatched= */ false) .getMatchingApksFromVariant(variant, bundleVersion); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java index 5873dbe8..8832a569 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java @@ -40,6 +40,7 @@ public interface ApkSetWriter { void writeApkSet(BuildSdkApksResult toc) throws IOException; + /** Creates ApkSet writer which stores all splits uncompressed inside output directory. */ static ApkSetWriter directory(Path outputDirectory) { return new ApkSetWriter() { @@ -57,6 +58,7 @@ public void writeApkSet(BuildApksResult toc) throws IOException { public void writeApkSet(BuildSdkApksResult toc) throws IOException { Files.write(getSplitsDirectory().resolve(TABLE_OF_CONTENTS_FILE), toc.toByteArray()); } + }; } @@ -100,6 +102,7 @@ public void writeApkSet(BuildSdkApksResult toc) throws IOException { zipApkSet(apkRelativePaths, toc.toByteArray()); } + private void zipApkSet(ImmutableSet apkRelativePaths, byte[] tocBytes) throws IOException { try (ZipArchive zipArchive = new ZipArchive(outputFile)) { diff --git a/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java b/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java index 0bd9126d..f3e3e348 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java @@ -40,8 +40,16 @@ public static ImmutableList waitForAll(Iterable> futu // reason that we don't use Futures#allAsList() directly is it will fail-fast and will cause the // other existing futures to continue running until they fail (e.g. if they depend on the // temporary file which gets deleted then it will produce NoFileFound exception). - waitFor(Futures.whenAllComplete(futures).call(() -> null, directExecutor())); - return ImmutableList.copyOf(waitFor(Futures.allAsList(futures))); + try { + return ImmutableList.copyOf(waitFor(Futures.allAsList(futures))); + } catch (RuntimeException e) { + try { + waitFor(Futures.whenAllComplete(futures).call(() -> null, directExecutor())); + } catch (RuntimeException ignoredException) { + // Silently ignored - only report the very first Exception encountered. + } + throw e; + } } public static ImmutableMap waitForAll(Map> futures) { 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 7273283a..06def8be 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 @@ -158,6 +158,7 @@ public abstract class AndroidManifest { public static final String FITS_SYSTEM_WINDOWS_ATTRIBUTE_NAME = "fitsSystemWindows"; public static final String SRC_ATTRIBUTE_NAME = "src"; public static final String APP_COMPONENT_FACTORY_ATTRIBUTE_NAME = "appComponentFactory"; + public static final String AUTHORITIES_ATTRIBUTE_NAME = "authorities"; public static final String LEANBACK_FEATURE_NAME = "android.software.leanback"; public static final String TOUCHSCREEN_FEATURE_NAME = "android.hardware.touchscreen"; @@ -219,6 +220,7 @@ public abstract class AndroidManifest { public static final int FITS_SYSTEM_WINDOWS_RESOURCE_ID = 0x010100dd; public static final int SRC_RESOURCE_ID = 0x01010119; public static final int APP_COMPONENT_FACTORY_RESOURCE_ID = 0x0101057a; + public static final int AUTHORITIES_RESOURCE_ID = 0x01010018; // Matches the value of android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, used when turning // a manifest attribute which references a prerelease API version (e.g., "Q") into an integer. @@ -929,6 +931,12 @@ public Optional getAppComponentFactoryAttribute() { .map(XmlProtoAttribute::getValueAsString); } + /** Gets the Authorities class name if it is set in the AndroidManifest. */ + public Optional getAuthoritiesAttribute() { + return getApplicationAttribute(AUTHORITIES_RESOURCE_ID) + .map(XmlProtoAttribute::getValueAsString); + } + private Optional getPropertyValue(String attributeName) { return getApplicationElementChildElements(PROPERTY_ELEMENT_NAME).stream() .filter(element -> element.getAndroidAttribute(NAME_RESOURCE_ID).isPresent()) 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 c7afa2c4..cbe41b5f 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 @@ -80,6 +80,7 @@ import static java.util.stream.Collectors.joining; import com.android.tools.build.bundletool.model.manifestelements.Activity; +import com.android.tools.build.bundletool.model.manifestelements.Provider; import com.android.tools.build.bundletool.model.manifestelements.Receiver; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; @@ -393,6 +394,14 @@ public ManifestEditor addReceiver(Receiver receiver) { return this; } + @CanIgnoreReturnValue + public ManifestEditor addProvider(Provider provider) { + manifestElement + .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) + .addChildElement(provider.asXmlProtoElement().toBuilder()); + return this; + } + /** Adds uses-sdk-library tag to the manifest. */ @CanIgnoreReturnValue public ManifestEditor addUsesSdkLibraryElement( @@ -511,6 +520,24 @@ public ManifestEditor setDeliveryOptionsForRuntimeEnabledSdkModule() { return this; } + /** Sets distribution module that is required for a recovery module. */ + @CanIgnoreReturnValue + public ManifestEditor setDistributionModuleForRecoveryModule() { + manifestElement + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, MODULE_ELEMENT_NAME) + .addChildElement( + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) + .addChildElement( + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "on-demand"))) + .addChildElement( + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, FUSING_ELEMENT_NAME) + .addAttribute( + XmlProtoAttributeBuilder.create( + DISTRIBUTION_NAMESPACE_URI, INCLUDE_ATTRIBUTE_NAME) + .setValueAsBoolean(false))); + return this; + } + @CanIgnoreReturnValue public ManifestEditor removeUsesSdkElement() { manifestElement.removeChildrenElementsIf( 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 2e301b86..fad761a2 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 @@ -24,8 +24,8 @@ 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.RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion; -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.SourceStampConstants.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStampConstants.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; @@ -54,7 +54,7 @@ import com.android.bundle.Targeting.TextureCompressionFormatTargeting; import com.android.bundle.Targeting.VariantTargeting; 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.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.targeting.TargetedDirectorySegment; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.google.auto.value.AutoValue; 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 index 089bc5c9..78d6257c 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SourceStamp.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SourceStamp.java @@ -32,9 +32,6 @@ public abstract class SourceStamp { public static final String LOCAL_SOURCE = "http://localhost"; - public static final String STAMP_SOURCE_METADATA_KEY = "com.android.stamp.source"; - public static final String STAMP_TYPE_METADATA_KEY = "com.android.stamp.type"; - /** Returns the signing configuration used for signing the stamp. */ public abstract SigningConfiguration getSigningConfiguration(); @@ -64,12 +61,4 @@ public abstract static class Builder { public abstract SourceStamp build(); } - - /** Type of stamp generated. */ - public enum StampType { - // Stamp generated for APKs intended for distribution. - STAMP_TYPE_DISTRIBUTION_APK, - // Stamp generated for standalone APKs. - STAMP_TYPE_STANDALONE_APK, - } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/SourceStampConstants.java b/src/main/java/com/android/tools/build/bundletool/model/SourceStampConstants.java new file mode 100644 index 00000000..96e901dc --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/SourceStampConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model; + +/** Constants for Source Stamp */ +public final class SourceStampConstants { + + public static final String STAMP_SOURCE_METADATA_KEY = "com.android.stamp.source"; + public static final String STAMP_TYPE_METADATA_KEY = "com.android.stamp.type"; + + /** Type of stamp generated. */ + public enum StampType { + // Stamp generated for APKs intended for distribution. + STAMP_TYPE_DISTRIBUTION_APK, + // Stamp generated for standalone APKs. + STAMP_TYPE_STANDALONE_APK, + } + + private SourceStampConstants() {} +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java new file mode 100644 index 00000000..390328b8 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model.manifestelements; + +import static com.android.tools.build.bundletool.model.AndroidManifest.AUTHORITIES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.AUTHORITIES_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.EXPORTED_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.EXPORTED_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; + +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import java.util.Optional; + +/** + * Represents Provider element of Android manifest. + * + *

This is not an exhaustive representation, some attributes and child elements might be missing. + */ +@Immutable +@AutoValue +@AutoValue.CopyAnnotations +public abstract class Provider { + abstract Optional getName(); + + abstract Optional getExported(); + + abstract Optional> getAuthorities(); + + public static Builder builder() { + return new AutoValue_Provider.Builder(); + } + + @Memoized + public XmlProtoElement asXmlProtoElement() { + XmlProtoElementBuilder elementBuilder = XmlProtoElementBuilder.create(PROVIDER_ELEMENT_NAME); + setNameAttribute(elementBuilder); + setExportedAttribute(elementBuilder); + setAuthoritiesAttribute(elementBuilder); + return elementBuilder.build(); + } + + private void setNameAttribute(XmlProtoElementBuilder elementBuilder) { + if (getName().isPresent()) { + elementBuilder + .getOrCreateAndroidAttribute(NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) + .setValueAsString(getName().get()); + } + } + + private void setExportedAttribute(XmlProtoElementBuilder elementBuilder) { + if (getExported().isPresent()) { + elementBuilder + .getOrCreateAndroidAttribute(EXPORTED_ATTRIBUTE_NAME, EXPORTED_RESOURCE_ID) + .setValueAsBoolean(getExported().get()); + } + } + + private void setAuthoritiesAttribute(XmlProtoElementBuilder elementBuilder) { + if (getAuthorities().isPresent()) { + elementBuilder + .getOrCreateAndroidAttribute(AUTHORITIES_ATTRIBUTE_NAME, AUTHORITIES_RESOURCE_ID) + .setValueAsString(String.join(";", getAuthorities().get())); + } + } + + /** Builder for Activity. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setName(String name); + + public abstract Builder setExported(boolean exported); + + public abstract Builder setAuthorities(ImmutableList authorities); + + public abstract Provider build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java index 0a2feeac..04b7cbbb 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "1.13.1"; + private static final String CURRENT_VERSION = "1.13.2"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java b/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java index 5e502abc..ce8f05ae 100644 --- a/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java +++ b/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java @@ -83,7 +83,22 @@ public abstract class ApkOptimizations { .setUncompressDexFiles(true) .setUncompressedDexTargetSdk(UncompressedDexTargetSdk.SDK_31) .build()) - .build(); + .put( + Version.of("1.13.2"), + ApkOptimizations.builder() + .setSplitDimensions( + ImmutableSet.of( + ABI, + SCREEN_DENSITY, + TEXTURE_COMPRESSION_FORMAT, + LANGUAGE, + DEVICE_TIER)) + .setUncompressNativeLibraries(true) + .setStandaloneDimensions(ImmutableSet.of(ABI, SCREEN_DENSITY)) + .setUncompressDexFiles(true) + .setUncompressedDexTargetSdk(UncompressedDexTargetSdk.SDK_31) + .build()) + .buildOrThrow(); /** List of dimensions supported by asset modules. */ private static final ImmutableSet DIMENSIONS_SUPPORTED_BY_ASSET_MODULES = diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundlePreprocessorModule.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundlePreprocessorModule.java index f4226123..1e38f3c4 100644 --- a/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundlePreprocessorModule.java +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/AppBundlePreprocessorModule.java @@ -36,8 +36,8 @@ static ImmutableList providePreprocessors( EmbeddedApkSigningPreprocessor embeddedApkSigningPreprocessor, EntryCompressionPreprocessor entryCompressionPreprocessor, LocalTestingPreprocessor localTestingPreprocessor, - RuntimeEnabledSdkTablePreprocessor runtimeEnabledSdkTablePreprocessor, RuntimeEnabledSdkDependencyPreprocessor runtimeEnabledSdkDependencyPreprocessor, + LocalRuntimeEnabledSdkConfigPreprocessor localRuntimeEnabledSdkConfigPreprocessor, BuildApksCommand command) { ImmutableList.Builder preprocessors = ImmutableList.builder() @@ -45,8 +45,8 @@ static ImmutableList providePreprocessors( appBundle64BitNativeLibrariesPreprocessor, embeddedApkSigningPreprocessor, entryCompressionPreprocessor, - runtimeEnabledSdkTablePreprocessor, - runtimeEnabledSdkDependencyPreprocessor); + runtimeEnabledSdkDependencyPreprocessor, + localRuntimeEnabledSdkConfigPreprocessor); if (command.getLocalTestingMode()) { preprocessors.add(localTestingPreprocessor); } diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalRuntimeEnabledSdkConfigPreprocessor.java b/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalRuntimeEnabledSdkConfigPreprocessor.java new file mode 100644 index 00000000..2395c246 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/preprocessors/LocalRuntimeEnabledSdkConfigPreprocessor.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.preprocessors; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.function.Function.identity; + +import com.android.bundle.RuntimeEnabledSdkConfigProto.CertificateOverride; +import com.android.bundle.RuntimeEnabledSdkConfigProto.LocalDeploymentRuntimeEnabledSdkConfig; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Optional; +import javax.inject.Inject; + +/** + * Preprocessor that overrides runtime-enabled SDK config of the app bundle for local deployment. + */ +public class LocalRuntimeEnabledSdkConfigPreprocessor implements AppBundlePreprocessor { + + private final ImmutableMap certificateOverridesBySdkName; + private final String defaultCertificateOverride; + + @Inject + LocalRuntimeEnabledSdkConfigPreprocessor( + Optional localRuntimeEnabledSdkConfig) { + this.defaultCertificateOverride = + localRuntimeEnabledSdkConfig + .map(config -> config.getCertificateOverrides().getDefaultCertificateOverride()) + .orElse(""); + this.certificateOverridesBySdkName = + localRuntimeEnabledSdkConfig + .map(LocalRuntimeEnabledSdkConfigPreprocessor::unnestCertificateOverrides) + .orElse(ImmutableMap.of()); + } + + @Override + public AppBundle preprocess(AppBundle bundle) { + if (certificateOverridesBySdkName.isEmpty() && defaultCertificateOverride.isEmpty()) { + return bundle; + } + ImmutableList updatedModules = + bundle.getModules().values().stream() + .map(this::updateRuntimeEnabledSdkConfig) + .collect(toImmutableList()); + return bundle.toBuilder() + .setRawModules(updatedModules) + .setRuntimeEnabledSdkDependencies(getAllRuntimeEnabledSdkDependencies(updatedModules)) + .build(); + } + + private BundleModule updateRuntimeEnabledSdkConfig(BundleModule module) { + if (module.getRuntimeEnabledSdkConfig().isPresent()) { + return module.toBuilder() + .setRuntimeEnabledSdkConfig( + updateRuntimeEnabledSdkConfig(module.getRuntimeEnabledSdkConfig().get())) + .build(); + } + return module; + } + + private RuntimeEnabledSdkConfig updateRuntimeEnabledSdkConfig(RuntimeEnabledSdkConfig config) { + RuntimeEnabledSdkConfig.Builder configBuilder = config.toBuilder(); + configBuilder.getRuntimeEnabledSdkBuilderList().forEach(this::updateRuntimeEnabledSdk); + return configBuilder.build(); + } + + private void updateRuntimeEnabledSdk(RuntimeEnabledSdk.Builder runtimeEnabledSdkBuilder) { + String sdkPackageName = runtimeEnabledSdkBuilder.getPackageName(); + if (certificateOverridesBySdkName.containsKey(sdkPackageName)) { + runtimeEnabledSdkBuilder.setCertificateDigest( + certificateOverridesBySdkName.get(sdkPackageName)); + } else if (!defaultCertificateOverride.isEmpty()) { + runtimeEnabledSdkBuilder.setCertificateDigest(defaultCertificateOverride); + } + } + + private static ImmutableMap unnestCertificateOverrides( + LocalDeploymentRuntimeEnabledSdkConfig localRuntimeEnabledSdkConfig) { + return localRuntimeEnabledSdkConfig + .getCertificateOverrides() + .getPerSdkCertificateOverrideList() + .stream() + .collect( + toImmutableMap( + CertificateOverride::getSdkPackageName, CertificateOverride::getCertificateDigest)); + } + + private ImmutableMap getAllRuntimeEnabledSdkDependencies( + ImmutableList modules) { + return modules.stream() + .filter(module -> module.getRuntimeEnabledSdkConfig().isPresent()) + .map(module -> module.getRuntimeEnabledSdkConfig().get()) + .flatMap( + runtimeEnabledSdkConfig -> runtimeEnabledSdkConfig.getRuntimeEnabledSdkList().stream()) + .collect(toImmutableMap(RuntimeEnabledSdk::getPackageName, identity())); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex b/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex deleted file mode 100644 index f7c6436a..00000000 Binary files a/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex and /dev/null differ diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/AndroidResourceRenamer.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/AndroidResourceRenamer.java new file mode 100644 index 00000000..b9733868 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/AndroidResourceRenamer.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.sdkmodule; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.Entry; +import com.android.aapt.Resources.FileReference; +import com.android.aapt.Resources.Package; +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Type; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +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.InvalidBundleException; +import com.android.tools.build.bundletool.model.utils.ResourcesUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import java.io.IOException; +import java.io.UncheckedIOException; + +/** + * Renames android resource entries of the given SDK module, to avoid file name clashes between the + * app and the SDK resources in non-sdk-runtime variant. + */ +final class AndroidResourceRenamer { + + private final String sdkPackageNameHash; + + AndroidResourceRenamer(SdkModulesConfig sdkModulesConfig) { + this.sdkPackageNameHash = getAlphaNumericHash(sdkModulesConfig.getSdkPackageName()); + } + + BundleModule renameAndroidResources(BundleModule module) { + if (!module.getResourceTable().isPresent()) { + return module; + } + ImmutableSet androidResourcePaths = + ResourcesUtils.getAllFileReferences(module.getResourceTable().get()); + // Rename resource entries, and keep all other entries unchanged. + ImmutableList newEntries = + module.getEntries().stream() + .map( + entry -> + androidResourcePaths.contains(entry.getPath()) + ? updateAndroidResourceEntryPath(entry) + : entry) + .collect(toImmutableList()); + return module.toBuilder() + // Rename files in the resource table. + .setResourceTable(renameInResourceTable(module.getResourceTable().get())) + .setRawEntries(newEntries) + .build(); + } + + private ModuleEntry updateAndroidResourceEntryPath(ModuleEntry androidResourceEntry) { + return androidResourceEntry.toBuilder() + .setPath(renamedResourcePath(androidResourceEntry.getPath())) + .build(); + } + + private ResourceTable renameInResourceTable(ResourceTable resourceTable) { + ResourceTable.Builder renamedResourceTableBuilder = resourceTable.toBuilder(); + renameInResourceTable(renamedResourceTableBuilder); + return renamedResourceTableBuilder.build(); + } + + private void renameInResourceTable(ResourceTable.Builder resourceTable) { + resourceTable.getPackageBuilderList().forEach(this::renameInPackage); + } + + private void renameInPackage(Package.Builder resourceTablePackage) { + resourceTablePackage.getTypeBuilderList().forEach(this::renameInType); + } + + private void renameInType(Type.Builder type) { + type.getEntryBuilderList().forEach(this::renameInEntry); + } + + private void renameInEntry(Entry.Builder entry) { + entry.getConfigValueBuilderList().forEach(this::renameInConfigValue); + } + + private void renameInConfigValue(ConfigValue.Builder configValue) { + if (!configValue.getValue().getItem().hasFile()) { + return; + } + renameInFile(configValue.getValueBuilder().getItemBuilder().getFileBuilder()); + } + + private void renameInFile(FileReference.Builder file) { + file.setPath(renamedResourcePath(ZipPath.create(file.getPath())).toString()); + } + + private ZipPath renamedResourcePath(ZipPath oldPath) { + if (oldPath == null + || oldPath.getParent() == null + || oldPath.getParent().equals(ZipPath.ROOT)) { + throw InvalidBundleException.createWithUserMessage( + "Android resource entry with unexpected path: " + oldPath); + } + return oldPath.getParent().resolve(sdkPackageNameHash + oldPath.getFileName()); + } + + private static String getAlphaNumericHash(String packageName) { + String result; + try { + result = + ByteSource.wrap(packageName.getBytes(UTF_8)) + .hash(Hashing.farmHashFingerprint64()) + .toString(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when calculating the hash.", e); + } + return result; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java index f673c764..0dc8d311 100644 --- a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java @@ -33,6 +33,7 @@ public final class SdkModuleToAppBundleModuleConverter { private final ResourceTablePackageIdRemapper resourceTablePackageIdRemapper; private final XmlPackageIdRemapper xmlPackageIdRemapper; private final DexAndResourceRepackager dexAndResourceRepackager; + private final AndroidResourceRenamer androidResourceRenamer; private final AndroidManifest appBaseModuleManifest; public SdkModuleToAppBundleModuleConverter( @@ -46,6 +47,7 @@ public SdkModuleToAppBundleModuleConverter( new XmlPackageIdRemapper(sdkDependencyConfig.getResourcesPackageId()); this.dexAndResourceRepackager = new DexAndResourceRepackager(sdkModule.getSdkModulesConfig().get(), sdkDependencyConfig); + this.androidResourceRenamer = new AndroidResourceRenamer(sdkModule.getSdkModulesConfig().get()); this.appBaseModuleManifest = appBaseModuleManifest; } @@ -54,9 +56,10 @@ public SdkModuleToAppBundleModuleConverter( * Bundle as a removable install-time module. */ public BundleModule convert() { - return repackageDexAndJavaResources( - remapResourceIdsInResourceTable( - remapResourceIdsInXmlResources(convertNameTypeAndManifest(sdkModule)))); + return renameAndroidResources( + repackageDexAndJavaResources( + remapResourceIdsInResourceTable( + remapResourceIdsInXmlResources(convertNameTypeAndManifest(sdkModule))))); } private BundleModule remapResourceIdsInResourceTable(BundleModule module) { @@ -71,6 +74,10 @@ private BundleModule repackageDexAndJavaResources(BundleModule module) { return dexAndResourceRepackager.repackage(module); } + private BundleModule renameAndroidResources(BundleModule module) { + return androidResourceRenamer.renameAndroidResources(module); + } + private BundleModule convertNameTypeAndManifest(BundleModule module) { // We are using modified SDK package name as a new module name. Dots are removed because special // characters are not allowed in module names. diff --git a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java index ff794bc9..4ff98f9a 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java @@ -26,10 +26,11 @@ import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.SourceStamp; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector; import com.android.tools.build.bundletool.splitters.CodeTransparencyInjector; +import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; @@ -47,6 +48,7 @@ public class StandaloneApksGenerator { private final ModuleSplitsToShardMerger shardsMerger; private final CodeTransparencyInjector codeTransparencyInjector; private final BinaryArtProfilesInjector binaryArtProfilesInjector; + private final RuntimeEnabledSdkTableInjector runtimeEnabledSdkTableInjector; @Inject public StandaloneApksGenerator( @@ -60,8 +62,10 @@ public StandaloneApksGenerator( this.sharder = sharder; this.shardsMerger = shardsMerger; this.codeTransparencyInjector = new CodeTransparencyInjector(appBundle); - binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); + this.binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); + this.runtimeEnabledSdkTableInjector = new RuntimeEnabledSdkTableInjector(appBundle); } + ; /** * Generates sharded APKs from the input modules. @@ -103,6 +107,7 @@ public ImmutableList generateStandaloneApks( .map(this::writeSourceStampInManifest) .map(codeTransparencyInjector::inject) .map(binaryArtProfilesInjector::inject) + .map(runtimeEnabledSdkTableInjector::inject) .collect(toImmutableList()); } diff --git a/src/main/java/com/android/tools/build/bundletool/shards/SystemApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/shards/SystemApksGenerator.java index 053c3351..8ebc23a8 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/SystemApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/SystemApksGenerator.java @@ -39,6 +39,7 @@ import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.splitters.BinaryArtProfilesInjector; import com.android.tools.build.bundletool.splitters.CodeTransparencyInjector; +import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -58,6 +59,7 @@ public class SystemApksGenerator { private final Optional deviceSpec; private final CodeTransparencyInjector codeTransparencyInjector; private final BinaryArtProfilesInjector binaryArtProfilesInjector; + private final RuntimeEnabledSdkTableInjector runtimeEnabledSdkTableInjector; @Inject public SystemApksGenerator( @@ -71,7 +73,8 @@ public SystemApksGenerator( this.shardsMerger = shardsMerger; this.deviceSpec = deviceSpec; this.codeTransparencyInjector = new CodeTransparencyInjector(appBundle); - binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); + this.binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); + this.runtimeEnabledSdkTableInjector = new RuntimeEnabledSdkTableInjector(appBundle); } /** @@ -102,6 +105,7 @@ public ImmutableList generateSystemApks( .map(module -> applyUncompressedOptimizations(module, apkOptimizations)) .map(codeTransparencyInjector::inject) .map(binaryArtProfilesInjector::inject) + .map(runtimeEnabledSdkTableInjector::inject) .collect(toImmutableList()); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java b/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java index 2a2452b1..0266dd41 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjector.java @@ -56,6 +56,7 @@ public ModuleSplit inject(ModuleSplit split) { content -> builder.addEntry( ModuleEntry.builder() + .setForceUncompressed(true) .setContent(content) .setPath(ZipPath.create(APK_LOCATION).resolve(BINARY_ART_PROFILE_NAME)) .build())); @@ -63,6 +64,7 @@ public ModuleSplit inject(ModuleSplit split) { content -> builder.addEntry( ModuleEntry.builder() + .setForceUncompressed(true) .setContent(content) .setPath(ZipPath.create(APK_LOCATION).resolve(BINARY_ART_PROFILE_METADATA_NAME)) .build())); 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 0c40dbbf..70a495f1 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 @@ -45,7 +45,7 @@ import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.ResourceId; import com.android.tools.build.bundletool.model.ResourceTableEntry; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.SuffixManager; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.version.Version; @@ -83,6 +83,7 @@ public class ModuleSplitter { private final PinSpecInjector pinSpecInjector; private final CodeTransparencyInjector codeTransparencyInjector; private final BinaryArtProfilesInjector binaryArtProfilesInjector; + private final RuntimeEnabledSdkTableInjector runtimeEnabledSdkTableInjector; @VisibleForTesting public static ModuleSplitter createForTest(BundleModule module, Version bundleVersion) { @@ -157,6 +158,7 @@ private ModuleSplitter( this.pinSpecInjector = new PinSpecInjector(module); this.codeTransparencyInjector = new CodeTransparencyInjector(appBundle); this.binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); + this.runtimeEnabledSdkTableInjector = new RuntimeEnabledSdkTableInjector(appBundle); this.allModuleNames = allModuleNames; this.stampSource = stampSource; this.stampType = stampType; @@ -261,6 +263,7 @@ private ImmutableList splitModuleInternal() { .map(pinSpecInjector::inject) .map(codeTransparencyInjector::inject) .map(binaryArtProfilesInjector::inject) + .map(runtimeEnabledSdkTableInjector::inject) .map(this::addApkTargetingForSigningConfiguration) .map(moduleSplit -> addDefaultSdkApkTargeting(moduleSplit, masterSplitMinSdk)) .map(this::writeSplitIdInManifest) diff --git a/src/main/java/com/android/tools/build/bundletool/preprocessors/RuntimeEnabledSdkTablePreprocessor.java b/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java similarity index 70% rename from src/main/java/com/android/tools/build/bundletool/preprocessors/RuntimeEnabledSdkTablePreprocessor.java rename to src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java index 1d4fb150..69fc9c4e 100644 --- a/src/main/java/com/android/tools/build/bundletool/preprocessors/RuntimeEnabledSdkTablePreprocessor.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License */ -package com.android.tools.build.bundletool.preprocessors; +package com.android.tools.build.bundletool.splitters; import static com.android.tools.build.bundletool.sdkmodule.DexAndResourceRepackager.getCompatSdkConfigPathInAssets; -import static com.google.common.collect.ImmutableList.toImmutableList; import static java.nio.charset.StandardCharsets.UTF_8; import com.android.tools.build.bundletool.model.AppBundle; -import com.android.tools.build.bundletool.model.BundleModule; 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.ZipPath; import com.android.tools.build.bundletool.xml.XmlUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteSource; -import javax.inject.Inject; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; @@ -34,8 +34,9 @@ import org.w3c.dom.Node; /** - * Preprocessor that generates RuntimeEnabledSdkTable.xml config for app bundles that have - * runtime-enabled SDK dependencies. + * Injects RuntimeEnabledSdkTable.xml config into the main split of the base module, as well as + * standalone splits, in the backwards-compatible variant of apps that have runtime-enabled SDK + * dependencies. * *

RuntimeEnabledSdkTable.xml contains paths to compat SDK config files inside the assets * directory. There is 1 compat SDK config file per runtime-enabled SDK dependency of the app. Here @@ -54,45 +55,48 @@ * * } */ -public class RuntimeEnabledSdkTablePreprocessor implements AppBundlePreprocessor { +public final class RuntimeEnabledSdkTableInjector { - private static final String RUNTIME_ENABLED_SDK_TABLE_FILE_PATH = + @VisibleForTesting + public static final String RUNTIME_ENABLED_SDK_TABLE_FILE_PATH = "assets/RuntimeEnabledSdkTable.xml"; + private static final String RUNTIME_ENABLED_SDK_TABLE_ELEMENT_NAME = "runtime-enabled-sdk-table"; private static final String RUNTIME_ENABELD_SDK_ELEMENT_NAME = "runtime-enabled-sdk"; private static final String SDK_PACKAGE_NAME_ELEMENT_NAME = "package-name"; private static final String COMPAT_CONFIG_PATH_ELEMENT_NAME = "compat-config-path"; - @Inject - RuntimeEnabledSdkTablePreprocessor() {} + private final AppBundle appBundle; + + public RuntimeEnabledSdkTableInjector(AppBundle appBundle) { + this.appBundle = appBundle; + } - @Override - public AppBundle preprocess(AppBundle bundle) { - if (bundle.getRuntimeEnabledSdkDependencies().isEmpty()) { - return bundle; + public ModuleSplit inject(ModuleSplit split) { + if (appBundle.getRuntimeEnabledSdkDependencies().isEmpty() + || !shouldAddRuntimeEnabledSdkTable(split)) { + return split; } - BundleModule modifiedBaseModule = - bundle.getBaseModule().toBuilder() - .addEntry( - ModuleEntry.builder() - .setPath(ZipPath.create(RUNTIME_ENABLED_SDK_TABLE_FILE_PATH)) - .setContent( - ByteSource.wrap( - XmlUtils.documentToString( - getRuntimeEnabledSdkTable( - bundle.getRuntimeEnabledSdkDependencies().keySet())) - .getBytes(UTF_8))) - .build()) - .build(); - return bundle.toBuilder() - .setRawModules( - bundle.getFeatureModules().values().stream() - .filter(module -> !module.isBaseModule()) - .collect(toImmutableList())) - .addRawModule(modifiedBaseModule) + return split.toBuilder() + .addEntry( + ModuleEntry.builder() + .setPath(ZipPath.create(RUNTIME_ENABLED_SDK_TABLE_FILE_PATH)) + .setContent( + ByteSource.wrap( + XmlUtils.documentToString( + getRuntimeEnabledSdkTable( + appBundle.getRuntimeEnabledSdkDependencies().keySet())) + .getBytes(UTF_8))) + .build()) .build(); } + private boolean shouldAddRuntimeEnabledSdkTable(ModuleSplit split) { + return !split.getVariantTargeting().getSdkRuntimeTargeting().getRequiresSdkRuntime() + && (split.getSplitType() == SplitType.STANDALONE + || (split.isMasterSplit() && split.isBaseModuleSplit())); + } + private Document getRuntimeEnabledSdkTable(ImmutableSet sdkPackageNames) { Document runtimeEnabledSdkTable; try { diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java index 9397985c..70b9115f 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java @@ -25,7 +25,7 @@ import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.SourceStamp; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.version.Version; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/src/main/proto/build_stamp.proto b/src/main/proto/build_stamp.proto new file mode 100644 index 00000000..06cb276e --- /dev/null +++ b/src/main/proto/build_stamp.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package android.bundle; + +import "google/protobuf/timestamp.proto"; + +option java_package = "com.android.bundle"; + +// Build stamp metadata. +message BuildStampFile { + // The build stamp for this bundle. + BuildStamp build_stamp = 1; +} + +// A build stamp. +// Next tag: 8. +message BuildStamp { + // Source revision for the build, e.g. HEAD commit hash. + string source_revision = 1; + // ID of the build job which created this bundle. + string job_id = 2; + // URL to the build job which created this bundle. Does not need to be public, + // and probably will not be. + string job_url = 3; + // ID for the specific build, e.g. a UUID. + string build_id = 4; + // Build label: an arbitrary string set by the build system. May be used to + // embed a release label. + string label = 5; + // Time at which the build was started. + google.protobuf.Timestamp build_start_timestamp = 6; + + // Status of the working tree this bundle was built from. + enum WorktreeStatus { + WORKTREE_STATUS_UNSPECIFIED = 0; + // Clean. No uncommitted modifications or files. + WORKTREE_STATUS_CLEAN = 1; + // Dirty. One or more uncommitted modifications or files. + WORKTREE_STATUS_DIRTY = 2; + } + // Status of the working tree this bundle was built from. + WorktreeStatus worktree_status = 7; +} diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index ca7ae31c..b0da786c 100644 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -325,3 +325,4 @@ message RuntimeEnabledSdkDependency { // Required. int32 minor_version = 3; } + diff --git a/src/main/proto/runtime_enabled_sdk_config.proto b/src/main/proto/runtime_enabled_sdk_config.proto index c467ab49..e275bc0d 100644 --- a/src/main/proto/runtime_enabled_sdk_config.proto +++ b/src/main/proto/runtime_enabled_sdk_config.proto @@ -34,3 +34,31 @@ message RuntimeEnabledSdk { // remapped to. int32 resources_package_id = 5; } + +// Options for overriding parts of runtime-enabled SDK dependency configuration +// for local deployment and testing. +message LocalDeploymentRuntimeEnabledSdkConfig { + // Options for overriding SDK certificate digests in RuntimeEnabledSdkConfig + // of the app bundle. + CertificateOverrides certificate_overrides = 1; +} + +// Contains information on certificate digests that should be overridden in +// runtime-enabled SDK dependency config for local deployment. +message CertificateOverrides { + // Certificate digest override for specific SDKs. + repeated CertificateOverride per_sdk_certificate_override = 1; + // Default certificate override. If set, certificate digests of SDKs that are + // not present in per_sdk_certificate_override field will be overriden to this + // value. + optional string default_certificate_override = 2; +} + +// Contains information on certificate digest overrides for a specific SDK. +message CertificateOverride { + // Package name of the SDK that we want to override the certificate digest + // for. + string sdk_package_name = 1; + // Certificate digest that we should override to. + string certificate_digest = 2; +} 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 fa14b4bb..49969fd1 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 @@ -68,6 +68,9 @@ import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.ModuleMetadata; import com.android.bundle.Commands.Variant; +import com.android.bundle.RuntimeEnabledSdkConfigProto.CertificateOverride; +import com.android.bundle.RuntimeEnabledSdkConfigProto.CertificateOverrides; +import com.android.bundle.RuntimeEnabledSdkConfigProto.LocalDeploymentRuntimeEnabledSdkConfig; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; @@ -187,6 +190,7 @@ public class BuildApksCommandTest { private Path sdkArchivePath1; private Path sdkArchivePath2; private Path extractedSdkBundleModulesPath; + private Path localDeploymentRuntimeEnabledSdkConfigPath; private final AdbServer fakeAdbServer = mock(AdbServer.class); private final SystemEnvironmentProvider systemEnvironmentProvider = @@ -281,6 +285,8 @@ public void setUp() throws Exception { sdkArchivePath1 = tmpDir.resolve("archive1.asar"); sdkArchivePath2 = tmpDir.resolve("archive2.asar"); extractedSdkBundleModulesPath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + localDeploymentRuntimeEnabledSdkConfigPath = + tmpDir.resolve("local-runtime-enabled-sdk-config.json"); } @Test @@ -1452,6 +1458,54 @@ public void buildingViaFlagsAndBuilderHasSameResult_runtimeEnabledSdkArchivesSet assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test + public void buildingViaFlagsAndBuilderHasSameResult_localRuntimeEnabledSdkConfigSet() + throws Exception { + LocalDeploymentRuntimeEnabledSdkConfig config = + LocalDeploymentRuntimeEnabledSdkConfig.newBuilder() + .setCertificateOverrides( + CertificateOverrides.newBuilder() + .addPerSdkCertificateOverride( + CertificateOverride.newBuilder() + .setSdkPackageName("com.test.sdk1") + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .addPerSdkCertificateOverride( + CertificateOverride.newBuilder() + .setSdkPackageName("com.test.sdk2") + .setCertificateDigest(VALID_CERT_FINGERPRINT2))) + .build(); + Files.writeString( + localDeploymentRuntimeEnabledSdkConfigPath, JsonFormat.printer().print(config)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildApksCommand commandViaFlags = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-bundles=" + sdkBundlePath1, + "--local-runtime-enabled-sdk-config=" + + localDeploymentRuntimeEnabledSdkConfigPath), + new PrintStream(output), + systemEnvironmentProvider, + fakeAdbServer); + + BuildApksCommand.Builder commandViaBuilder = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setRuntimeEnabledSdkBundlePaths(ImmutableSet.of(sdkBundlePath1)) + .setLocalDeploymentRuntimeEnabledSdkConfig(config) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()); + + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + @Test public void buildingViaFlagsAndBuilderHasSameResult_customStorePackage() throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -1722,8 +1776,8 @@ public void populateLineage_apkFile() throws Exception { SigningCertificateLineage lineage = new SigningCertificateLineage.Builder(oldestSignerConfig, signerConfig).build(); - com.android.tools.build.bundletool.model.SignerConfig oldestSigner = - com.android.tools.build.bundletool.model.SignerConfig.builder() + SignerConfig oldestSigner = + SignerConfig.builder() .setPrivateKey(oldestSignerPrivateKey) .setCertificates(ImmutableList.of(oldestSignerCertificate)) .build(); @@ -1908,6 +1962,46 @@ public void settingBothSdkBundlesAndSdkArchives_fromBuilder_throws() { + " archives, but both were set."); } + @Test + public void localDeploymentRuntimeEnabledSdkConfig_withoutSdkDependencies_fromFlags_throws() { + Throwable e = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--local-runtime-enabled-sdk-config=" + + localDeploymentRuntimeEnabledSdkConfigPath), + fakeAdbServer)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "'local-runtime-enabled-sdk-config' flag can only be set together with 'sdk-bundles' or" + + " 'sdk-archives' flags."); + } + + @Test + public void localDeploymentRuntimeEnabledSdkConfig_withoutSdkDependencies_fromBuilder_throws() { + Throwable e = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + .setLocalDeploymentRuntimeEnabledSdkConfig( + LocalDeploymentRuntimeEnabledSdkConfig.getDefaultInstance()) + .build()); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Using --local-deployment-runtime-enabled-sdk-config flag requires either" + + " --sdk-bundles or --sdk-archives flag to be also present."); + } + @Test public void sdkBundleFileDoesNotExist_throws() throws Exception { // Creating SDK bundle for sdkBundlePath1, but not for SdkBundlePath2. @@ -2037,6 +2131,65 @@ public void badSdkArchiveFileExtension_throws() throws Exception { .contains("ASAR file 'sdk_archive.aab' is expected to have '.asar' extension."); } + @Test + public void sdkBundlesSetInUnsupportedMode_throws() throws Exception { + createSdkBundle(sdkBundlePath1, "com.test.sdk2", /* majorVersion= */ 2, /* minorVersion= */ 3); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-bundles=" + sdkBundlePath1, + "--mode=ARCHIVE"), + fakeAdbServer); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("runtimeEnabledSdkBundlePaths can not be present in 'ARCHIVE' mode."); + } + + @Test + public void sdkArchivessSetInUnsupportedMode_throws() throws Exception { + createSdkArchive( + sdkArchivePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()); + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-archives=" + sdkArchivePath1, + "--mode=ARCHIVE"), + fakeAdbServer); + + Exception e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("runtimeEnabledSdkArchivePaths can not be present in 'ARCHIVE' mode."); + } + @Test public void sdkBundleZipMissingModulesFile_sdkBundleZipFileValidationFails() throws Exception { createZipBuilderForSdkBundle().writeTo(sdkBundlePath1); @@ -2267,6 +2420,28 @@ public void missingSdkBundleInInput_throws() throws Exception { .contains("App bundle depends on SDK 'com.test.sdk2', but no SDK bundle was provided."); } + @Test + public void appBundleHasSdkDeps_noSdkBundleInInput_modeWithoutSdkRuntimeVariant_succeeds() + throws Exception { + createAppBundleWithRuntimeEnabledSdkConfig( + bundlePath, + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()); + + BuildApksCommand.fromFlags( + new FlagParser() + .parse("--bundle=" + bundlePath, "--output=" + outputFilePath, "--mode=INSTANT"), + fakeAdbServer) + .execute(); + } + @Test public void missingSdkArchiveInInput_throws() throws Exception { createSdkArchive( 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 910d2f71..e2d36466 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 @@ -35,7 +35,7 @@ import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.model.OptimizationDimension.TEXTURE_COMPRESSION_FORMAT; -import static com.android.tools.build.bundletool.model.SourceStamp.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStampConstants.STAMP_SOURCE_METADATA_KEY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MDPI_VALUE; import static com.android.tools.build.bundletool.model.utils.ResultUtils.apexApkVariants; import static com.android.tools.build.bundletool.model.utils.ResultUtils.archivedApkVariants; diff --git a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java index 9a9a7e98..cdf49a98 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java @@ -1253,27 +1253,27 @@ public void getSizeTotal_withAssetModules_selectedModules() throws Exception { .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of(Dimension.ABI, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.SDK)) - .setModules(ImmutableSet.of("asset2")) + .setModules(ImmutableSet.of("base", "asset2")) .build() .getSizeTotal(new PrintStream(outputStream)); - // Selects all install-time modules (base and asset1) and the ones explicitly selected (asset2). + // Selects base module and the ones explicitly selected (asset2). assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) .asList() .containsExactly( "SDK,ABI,TEXTURE_COMPRESSION_FORMAT,MIN,MAX", String.format( "%s,%s,%s,%d,%d", - "21-", "x86_64", "astc", 5 * compressedApkSize, 5 * compressedApkSize), + "21-", "x86_64", "astc", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", - "21-", "x86_64", "etc2", 5 * compressedApkSize, 5 * compressedApkSize), + "21-", "x86_64", "etc2", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", - "21-", "x86", "astc", 5 * compressedApkSize, 5 * compressedApkSize), + "21-", "x86", "astc", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", - "21-", "x86", "etc2", 5 * compressedApkSize, 5 * compressedApkSize)); + "21-", "x86", "etc2", 3 * compressedApkSize, 3 * compressedApkSize)); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index 2e640c34..45243e4f 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -18,6 +18,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.APP_COMPONENT_FACTORY_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.APP_COMPONENT_FACTORY_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.AUTHORITIES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.AUTHORITIES_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.DEBUGGABLE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.DESCRIPTION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.DESCRIPTION_RESOURCE_ID; @@ -1448,4 +1450,23 @@ public void getAppComponentFactoryAttribute_present() { assertThat(androidManifest.getAppComponentFactoryAttribute()) .hasValue("my.package.customFactory"); } + + @Test + public void getAuthoritiesAttribute_present() { + AndroidManifest androidManifest = + AndroidManifest.create( + xmlNode( + xmlElement( + "manifest", + xmlNode( + xmlElement( + "application", + xmlAttribute( + ANDROID_NAMESPACE_URI, + AUTHORITIES_ATTRIBUTE_NAME, + AUTHORITIES_RESOURCE_ID, + "my.package.customAuthority")))))); + + assertThat(androidManifest.getAuthoritiesAttribute()).hasValue("my.package.customAuthority"); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index 3e741166..7d5a655f 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -76,6 +76,7 @@ import com.android.aapt.Resources.XmlNode; import com.android.tools.build.bundletool.TestData; import com.android.tools.build.bundletool.model.manifestelements.Activity; +import com.android.tools.build.bundletool.model.manifestelements.Provider; import com.android.tools.build.bundletool.model.manifestelements.Receiver; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; @@ -117,7 +118,6 @@ public void setMinSdkVersion_nonExistingElement_created() throws Exception { 123)))); } - @Test public void setMinSdkVersion_existingAttribute_adjusted() throws Exception { AndroidManifest androidManifest = @@ -748,6 +748,18 @@ public void addReceiver() throws Exception { .containsExactly(receiverXmlNode); } + @Test + public void addProvider() throws Exception { + Provider provider = Provider.builder().setName("providerName").build(); + XmlNode providerXmlNode = + XmlNode.newBuilder().setElement(provider.asXmlProtoElement().getProto()).build(); + AndroidManifest androidManifest = AndroidManifest.create(androidManifest("com.test.app")); + AndroidManifest editedManifest = androidManifest.toEditor().addProvider(provider).save(); + + assertThat(getApplicationElement(editedManifest).getChildList()) + .containsExactly(providerXmlNode); + } + @Test public void addUsesSdkLibraryElement() { AndroidManifest androidManifest = AndroidManifest.create(androidManifest("com.test.app")); @@ -1095,6 +1107,35 @@ public void setDeliveryOptionsForRuntimeEnabledSdkModule() { DISTRIBUTION_NAMESPACE_URI, INCLUDE_ATTRIBUTE_NAME, true)))))); } + @Test + public void setDistributionModuleForRecoveryModule() { + AndroidManifest androidManifest = AndroidManifest.create(xmlNode(xmlElement("manifest"))); + AndroidManifest editedManifest = + androidManifest.toEditor().setDistributionModuleForRecoveryModule().save(); + + XmlNode editedManifestRoot = editedManifest.getManifestRoot().getProto(); + assertThat(editedManifestRoot.hasElement()).isTrue(); + XmlElement manifestElement = editedManifestRoot.getElement(); + assertThat(manifestElement.getName()).isEqualTo("manifest"); + assertThat(manifestElement.getChildList()) + .containsExactly( + xmlNode( + xmlElement( + DISTRIBUTION_NAMESPACE_URI, + MODULE_ELEMENT_NAME, + xmlNode( + xmlElement( + DISTRIBUTION_NAMESPACE_URI, + DELIVERY_ELEMENT_NAME, + xmlNode(xmlElement(DISTRIBUTION_NAMESPACE_URI, "on-demand")))), + xmlNode( + xmlElement( + DISTRIBUTION_NAMESPACE_URI, + FUSING_ELEMENT_NAME, + xmlBooleanAttribute( + DISTRIBUTION_NAMESPACE_URI, INCLUDE_ATTRIBUTE_NAME, false)))))); + } + @Test public void removeUsesSdkElement() { AndroidManifest androidManifest = 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 dd97ec57..d5c1f136 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 @@ -22,8 +22,8 @@ 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.AndroidManifest.VERSION_MAJOR_RESOURCE_ID; -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.SourceStampConstants.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStampConstants.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; @@ -70,7 +70,7 @@ import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ManifestProtoUtils; diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditionsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditionsTest.java index 80e96093..af3e4db9 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditionsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/files/FilePreconditionsTest.java @@ -23,10 +23,7 @@ import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileHasExtension; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.Assume.assumeFalse; -import com.android.tools.build.bundletool.model.utils.OsPlatform; -import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -68,21 +65,6 @@ public void checkFileExistsAndReadable_nonExistent_fail() throws Exception { assertThat(exception).hasMessageThat().matches("File '.*' was not found."); } - @Test - public void checkFileExistsAndReadable_nonReadable_fail() throws Exception { - // "File.setReadable(false)" does not work on Windows. - assumeFalse(OsPlatform.getCurrentPlatform().equals(OsPlatform.WINDOWS)); - - File unreadableFile = tmp.newFile(); - unreadableFile.setReadable(false); - - IllegalArgumentException exception = - assertThrows( - IllegalArgumentException.class, - () -> checkFileExistsAndReadable(unreadableFile.toPath())); - assertThat(exception).hasMessageThat().matches("File '.*' is not readable."); - } - @Test public void checkFileExistsAndReadable_notAFile_fail() throws Exception { Path directory = tmp.newFolder().toPath(); diff --git a/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java b/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java index e066c8e1..b3b09647 100644 --- a/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java @@ -91,6 +91,24 @@ public void getDefaultOptimizations_0_2_0_onlySplitsByAbiDensityAndLanguage() { .build()); } + @Test + public void + getDefaultOptimizations_1_13_2_onlySplitsByAbiDensityTextureLanguageUncompressNativeLibsUncompressedDexDeviceTiers() { + ApkOptimizations defaultOptimizations = + ApkOptimizations.getDefaultOptimizationsForVersion(Version.of("1.13.2")); + assertThat(defaultOptimizations) + .isEqualTo( + ApkOptimizations.builder() + .setSplitDimensions( + ImmutableSet.of( + ABI, SCREEN_DENSITY, TEXTURE_COMPRESSION_FORMAT, LANGUAGE, DEVICE_TIER)) + .setUncompressNativeLibraries(true) + .setStandaloneDimensions(ImmutableSet.of(ABI, SCREEN_DENSITY)) + .setUncompressDexFiles(true) + .setUncompressedDexTargetSdk(UncompressedDexTargetSdk.SDK_31) + .build()); + } + @Test public void getSplitDimensionsForAssetModules_returnsDimensionsSupportedByAssetModules() { ApkOptimizations optimizations = diff --git a/src/test/java/com/android/tools/build/bundletool/shards/ShardedApksFacadeTest.java b/src/test/java/com/android/tools/build/bundletool/shards/ShardedApksFacadeTest.java index 4dcc411f..c1b0721d 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/ShardedApksFacadeTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/ShardedApksFacadeTest.java @@ -18,8 +18,8 @@ import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; -import static com.android.tools.build.bundletool.model.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.SourceStampConstants.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStampConstants.STAMP_TYPE_METADATA_KEY; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.LDPI_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MDPI_VALUE; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; @@ -68,7 +68,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.SourceStamp; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; diff --git a/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java index a2e4c3ec..d5206f0e 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java @@ -77,6 +77,8 @@ import com.android.bundle.Config.StandaloneConfig; import com.android.bundle.Config.StandaloneConfig.DexMergingStrategy; import com.android.bundle.Config.SuffixStripping; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; @@ -84,6 +86,7 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.commands.BuildApksModule; import com.android.tools.build.bundletool.commands.CommandScoped; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleSplit; @@ -92,6 +95,8 @@ import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ResourceTableBuilder; import com.android.tools.build.bundletool.testing.TestModule; @@ -1573,6 +1578,44 @@ public void standaloneApks_injectBinaryArtProfiles() throws Exception { assertThat(actualContent).isEqualTo(content); } + @Test + public void bundleHasdSdkDeps_sdkTableConfigInjected() { + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig = + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest("certdigest")) + .build(); + BundleModule bundleModule = + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .setRuntimeEnabledSdkConfig(runtimeEnabledSdkConfig) + .build(); + AppBundle appBundle = new AppBundleBuilder().addModule(bundleModule).build(); + TestComponent.useTestModule(this, TestModule.builder().withAppBundle(appBundle).build()); + + ImmutableList shards = + standaloneApksGenerator.generateStandaloneApks( + ImmutableList.of(bundleModule), standaloneApkOptimizations(OptimizationDimension.ABI)); + + shards.forEach( + shard -> + assertThat( + shard.getEntries().stream() + .filter( + entry -> + entry + .getPath() + .equals( + ZipPath.create( + RuntimeEnabledSdkTableInjector + .RUNTIME_ENABLED_SDK_TABLE_FILE_PATH)))) + .hasSize(1)); + } + private static ApkOptimizations standaloneApkOptimizations(OptimizationDimension... dimensions) { return ApkOptimizations.builder() .setSplitDimensions(ImmutableSet.of()) diff --git a/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java index e7cdb876..ba049269 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/SystemApksGeneratorTest.java @@ -70,10 +70,13 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.tools.build.bundletool.commands.BuildApksModule; import com.android.tools.build.bundletool.commands.CommandScoped; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; @@ -83,6 +86,8 @@ import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ResourceTableBuilder; import com.android.tools.build.bundletool.testing.TestModule; @@ -1117,6 +1122,43 @@ public void systemApks_injectBinaryArtProfiles() throws Exception { assertThat(actualContent).isEqualTo(content); } + @Test + public void appBundleHasRuntimeEnabledSdkDependencies_injectsRuntimeEnabledSdkTable() + throws Exception { + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig = + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest("certdigest")) + .build(); + BundleModule baseModule = + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .setRuntimeEnabledSdkConfig(runtimeEnabledSdkConfig) + .build(); + AppBundle appBundle = new AppBundleBuilder().addModule(baseModule).build(); + TestComponent.useTestModule( + this, TestModule.builder().withDeviceSpec(DEVICE_SPEC).withAppBundle(appBundle).build()); + + ImmutableList shards = + systemApksGenerator.generateSystemApks( + /* modules= */ ImmutableList.of(baseModule), + /* modulesToFuse= */ ImmutableSet.of(BASE_MODULE_NAME), + splitOptimizations()); + + assertThat(shards).hasSize(1); + assertThat( + shards + .get(0) + .findEntry( + ZipPath.create( + RuntimeEnabledSdkTableInjector.RUNTIME_ENABLED_SDK_TABLE_FILE_PATH))) + .isPresent(); + } + private static ApkOptimizations splitOptimizations(OptimizationDimension... dimensions) { return ApkOptimizations.builder() .setSplitDimensions(ImmutableSet.copyOf(dimensions)) diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java index 265d46f2..987bcd52 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/BinaryArtProfilesInjectorTest.java @@ -76,9 +76,15 @@ public void mainSplitOfTheBaseModule_artProfileInjected() { .build(); ModuleSplit result = injector.inject(moduleSplit); - assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.prof"))) - .hasValue(BINARY_ART_PROFILE_CONTENT); - assertThat(getEntryContent(result, ZipPath.create("assets/dexopt/baseline.profm"))) + ZipPath apkProfilePath = ZipPath.create("assets/dexopt/baseline.prof"); + ZipPath apkProfileMetadataPath = ZipPath.create("assets/dexopt/baseline.profm"); + assertThat(result.findEntry(apkProfilePath).map(ModuleEntry::getForceUncompressed)) + .hasValue(true); + assertThat(result.findEntry(apkProfileMetadataPath).map(ModuleEntry::getForceUncompressed)) + .hasValue(true); + + assertThat(getEntryContent(result, apkProfilePath)).hasValue(BINARY_ART_PROFILE_CONTENT); + assertThat(getEntryContent(result, apkProfileMetadataPath)) .hasValue(BINARY_ART_PROFILE_METADATA_CONTENT); assertThat(getEntryContent(result, ZipPath.create("some.bin"))).hasValue(new byte[] {10, 9, 8}); } 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 fed3c1ec..493f0cc2 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 @@ -35,8 +35,8 @@ import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; import static com.android.tools.build.bundletool.model.OptimizationDimension.TEXTURE_COMPRESSION_FORMAT; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion; -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.SourceStampConstants.STAMP_SOURCE_METADATA_KEY; +import static com.android.tools.build.bundletool.model.SourceStampConstants.STAMP_TYPE_METADATA_KEY; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_M_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; @@ -128,7 +128,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.ResourceId; -import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.SourceStampConstants.StampType; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.utils.Versions; @@ -2447,6 +2447,48 @@ public void sdkRuntimeVariant_overridesMinSdkVersion() { .isEqualTo(androidManifest("com.test.app", withMinSdkVersion(SDK_SANDBOX_MIN_VERSION))); } + @Test + public void bundleHasdSdkDeps_nonSdkRuntimeVariant_baseModule_mainSplit_sdkTableConfigInjected() { + RuntimeEnabledSdkConfig runtimeEnabledSdkConfig = + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_DIGEST)) + .build(); + BundleModule testModule = + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .setRuntimeEnabledSdkConfig(runtimeEnabledSdkConfig) + .build(); + AppBundle appBundle = new AppBundleBuilder().addModule(testModule).build(); + ModuleSplitter moduleSplitter = + ModuleSplitter.createNoStamp( + testModule, + BUNDLETOOL_VERSION, + appBundle, + ApkGenerationConfiguration.getDefaultInstance(), + VariantTargeting.getDefaultInstance(), + ImmutableSet.of("base")); + + ImmutableList splits = moduleSplitter.splitModule(); + + ModuleSplit mainSplit = splits.stream().filter(ModuleSplit::isMasterSplit).findAny().get(); + assertThat( + mainSplit.getEntries().stream() + .filter( + entry -> + entry + .getPath() + .equals( + ZipPath.create( + RuntimeEnabledSdkTableInjector + .RUNTIME_ENABLED_SDK_TABLE_FILE_PATH)))) + .hasSize(1); + } + private ModuleSplit checkAndReturnTheOnlyMasterSplit(List splits) { int masterSplitsFound = 0; ModuleSplit masterSplit = null; diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java new file mode 100644 index 00000000..208bd4ad --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.splitters; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkRuntimeVariantTargeting; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.VariantTargeting; +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.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.Iterables; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RuntimeEnabledSdkTableInjectorTest { + + private static final String VALID_CERT_FINGERPRINT = + "96:C7:EC:89:3E:69:2A:25:BA:4D:EE:C1:84:E8:33:3F:34:7D:6D:12:26:A1:C1:AA:70:A2:8A:DB:75:3E:02:0A"; + + @Test + public void appBundleHasNoSdkDependencies_noChange() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app"))) + .addModule("feature", module -> module.setManifest(androidManifest("com.app"))) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.BASE_MODULE_NAME) + .setMasterSplit(true) + .setSplitType(SplitType.SPLIT) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result).isEqualTo(split); + } + + @Test + public void appBundleHasSdkDependencies_sdkRuntimeVariant_noChange() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build())) + .addModule("feature", module -> module.setManifest(androidManifest("com.app"))) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.BASE_MODULE_NAME) + .setMasterSplit(true) + .setSplitType(SplitType.SPLIT) + .setVariantTargeting(sdkRuntimeVariantTargeting()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result).isEqualTo(split); + } + + @Test + public void appBundleHasSdkDependencies_notMasterSplit_noChange() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build())) + .addModule("feature", module -> module.setManifest(androidManifest("com.app"))) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.BASE_MODULE_NAME) + .setMasterSplit(false) + .setSplitType(SplitType.SPLIT) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result).isEqualTo(split); + } + + @Test + public void appBundleHasSdkDependencies_notBaseModule_noChange() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build())) + .addModule("feature", module -> module.setManifest(androidManifest("com.app"))) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.create("notBaseModule")) + .setMasterSplit(true) + .setSplitType(SplitType.SPLIT) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result).isEqualTo(split); + } + + @Test + public void appBundleHasSdkDependencies_runtimeEnabledSdkTableAdded_splitVariant() + throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()) + .build(); + BundleModule featureModule = + new BundleModuleBuilder("feature") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk2") + .setVersionMajor(3) + .setVersionMinor(4) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()) + .build(); + AppBundle appBundle = + new AppBundleBuilder() + .addModule(baseModule) + .addModule(featureModule) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.BASE_MODULE_NAME) + .setMasterSplit(true) + .setSplitType(SplitType.SPLIT) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result.getEntries()).hasSize(1); + assertThat(Iterables.getOnlyElement(result.getEntries()).getPath()) + .isEqualTo(ZipPath.create("assets/RuntimeEnabledSdkTable.xml")); + assertThat( + Iterables.getOnlyElement(result.getEntries()) + .getContent() + .asCharSource(UTF_8) + .readLines()) + .containsExactly( + "", + " ", + " com.test.sdk1", + " RuntimeEnabledSdk-com.test.sdk1/CompatSdkConfig.xml", + " ", + " ", + " com.test.sdk2", + " RuntimeEnabledSdk-com.test.sdk2/CompatSdkConfig.xml", + " ", + ""); + } + + @Test + public void appBundleHasSdkDependencies_runtimeEnabledSdkTableAdded_standaloneVariant() + throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("assets/apk1.apk") + .addFile("assets/apk2.apk") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()) + .build(); + BundleModule featureModule = + new BundleModuleBuilder("feature") + .setManifest(androidManifest("com.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk2") + .setVersionMajor(3) + .setVersionMinor(4) + .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .build()) + .build(); + AppBundle appBundle = + new AppBundleBuilder() + .addModule(baseModule) + .addModule(featureModule) + .setBundleConfig(BundleConfig.getDefaultInstance()) + .build(); + RuntimeEnabledSdkTableInjector injector = new RuntimeEnabledSdkTableInjector(appBundle); + ModuleSplit split = + getModuleSplitBuilder() + .setModuleName(BundleModuleName.create("base")) + .setMasterSplit(false) + .setSplitType(SplitType.STANDALONE) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .build(); + + ModuleSplit result = injector.inject(split); + + assertThat(result.getEntries()).hasSize(1); + assertThat(Iterables.getOnlyElement(result.getEntries()).getPath()) + .isEqualTo(ZipPath.create("assets/RuntimeEnabledSdkTable.xml")); + assertThat( + Iterables.getOnlyElement(result.getEntries()) + .getContent() + .asCharSource(UTF_8) + .readLines()) + .containsExactly( + "", + " ", + " com.test.sdk1", + " RuntimeEnabledSdk-com.test.sdk1/CompatSdkConfig.xml", + " ", + " ", + " com.test.sdk2", + " RuntimeEnabledSdk-com.test.sdk2/CompatSdkConfig.xml", + " ", + ""); + } + + private static ModuleSplit.Builder getModuleSplitBuilder() { + return ModuleSplit.builder() + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setAndroidManifest( + AndroidManifest.create(androidManifest("com.test.app")) + .toEditor() + .setMinSdkVersion(28) + .save()); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java index 1b42fcb4..b5328b4f 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java @@ -325,6 +325,7 @@ public TestModule build() { .setAapt2Command(Aapt2Helper.getAapt2Command()) .setSdkBundlePath(bundlePath) .setOutputFile(outputPath); + if (signingConfig != null) { command.setSigningConfiguration(signingConfig); sdkCommand.setSigningConfiguration(signingConfig);