提交 a380c5bb 编写于 作者: I Iurii Makhno

Prepare for release 1.3.0.

上级 24df0e8b
......@@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool
## Releases
Latest release: [1.2.0](https://github.com/google/bundletool/releases)
Latest release: [1.3.0](https://github.com/google/bundletool/releases)
......@@ -21,17 +21,6 @@ apply plugin: "maven"
repositories {
jcenter()
google()
// We need to add this custom format repo because we use r8 version which is
// not available in gMaven yet. This repo can be removed once new version is
// uploaded to gMaven.
// Shrunk version of r8 is available there by the following path:
// https://storage.googleapis.com/r8-releases/raw/[revision]/r8lib.jar
ivy {
url "https://storage.googleapis.com/r8-releases/raw"
patternLayout {
artifact "[revision]/[artifact]lib.[ext]"
}
}
}
configurations {
......@@ -42,10 +31,11 @@ configurations {
// The repackaging rules are defined in the "shadowJar" task below.
dependencies {
compile "com.android.tools:r8:2.0.23"
compile "com.android.tools.build:apkzlib:4.0.0"
compile "com.android.tools.build:apksig:4.0.0"
compile "com.android.tools:r8:2.1.66"
compile "com.android.tools.build:apkzlib:4.2.0-alpha13"
compile "com.android.tools.build:apksig:4.2.0-alpha13"
compile "com.android.tools.ddms:ddmlib:26.2.0"
compile "com.android:zipflinger:4.2.0-alpha11"
shadow "com.android.tools.build:aapt2-proto:4.1.0-alpha01-6193524"
shadow "com.google.auto.value:auto-value-annotations:1.6.2"
......@@ -151,6 +141,7 @@ shadowJar {
// Package all the Android Gradle plugin dependencies that are compiled from
// source.
dependencies {
include(dependency('com.android:zipflinger:.*'))
include(dependency {
it.moduleGroup.startsWith('com.android.tools')
})
......@@ -180,8 +171,8 @@ task executableJar(type: ShadowJar) {
from({ zipTree(project.configurations.compileMacOs.singleFile) }) { into 'macos/' }
from({ zipTree(project.configurations.compileLinux.singleFile) }) { into 'linux/' }
configurations = [
project.configurations.runtime,
project.configurations.shadow
project.configurations.runtime,
project.configurations.shadow
]
manifest {
attributes 'Main-Class': 'com.android.tools.build.bundletool.BundleToolMain'
......
release_version = 1.2.0
release_version = 1.3.0
......@@ -15,13 +15,13 @@
*/
package com.android.tools.build.bundletool.commands;
import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM;
import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.android.bundle.Commands.LocalTestingInfo;
import com.android.bundle.Config.BundleConfig;
import com.android.bundle.Config.SuffixStripping;
import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode;
import com.android.tools.build.bundletool.commands.BuildApksCommand.SystemApkOption;
......@@ -40,7 +40,6 @@ import com.android.tools.build.bundletool.model.BundleModuleName;
import com.android.tools.build.bundletool.model.GeneratedApks;
import com.android.tools.build.bundletool.model.GeneratedAssetSlices;
import com.android.tools.build.bundletool.model.ModuleSplit;
import com.android.tools.build.bundletool.model.OptimizationDimension;
import com.android.tools.build.bundletool.model.SigningConfiguration;
import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException;
import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException;
......@@ -51,7 +50,6 @@ import com.android.tools.build.bundletool.model.utils.Versions;
import com.android.tools.build.bundletool.model.version.Version;
import com.android.tools.build.bundletool.model.version.VersionGuardedFeature;
import com.android.tools.build.bundletool.optimizations.ApkOptimizations;
import com.android.tools.build.bundletool.optimizations.OptimizationsMerger;
import com.android.tools.build.bundletool.preprocessors.LocalTestingPreprocessor;
import com.android.tools.build.bundletool.shards.ShardedApksFacade;
import com.android.tools.build.bundletool.splitters.ApkGenerationConfiguration;
......@@ -60,7 +58,6 @@ import com.android.tools.build.bundletool.splitters.ResourceAnalyzer;
import com.android.tools.build.bundletool.splitters.SplitApksGenerator;
import com.android.tools.build.bundletool.validation.AppBundleValidator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.file.Files;
......@@ -83,7 +80,7 @@ public final class BuildApksManager {
private final ApkSerializerManager apkSerializerManager;
private final SplitApksGenerator splitApksGenerator;
private final ShardedApksFacade shardedApksFacade;
private final OptimizationsMerger optimizationsMerger;
private final ApkOptimizations apkOptimizations;
@Inject
BuildApksManager(
......@@ -97,7 +94,7 @@ public final class BuildApksManager {
ApkSerializerManager apkSerializerManager,
SplitApksGenerator splitApksGenerator,
ShardedApksFacade shardedApksFacade,
OptimizationsMerger optimizationsMerger) {
ApkOptimizations apkOptimizations) {
this.appBundle = appBundle;
this.command = command;
this.bundletoolVersion = bundletoolVersion;
......@@ -108,7 +105,7 @@ public final class BuildApksManager {
this.splitApksGenerator = splitApksGenerator;
this.apkSerializerManager = apkSerializerManager;
this.shardedApksFacade = shardedApksFacade;
this.optimizationsMerger = optimizationsMerger;
this.apkOptimizations = apkOptimizations;
}
public void execute() throws IOException {
......@@ -208,13 +205,12 @@ public final class BuildApksManager {
ImmutableList<BundleModule> allModules = getModulesForStandaloneApks(appBundle);
return appBundle.isApex()
? shardedApksFacade.generateApexSplits(modulesToFuse(allModules))
: shardedApksFacade.generateSplits(
modulesToFuse(allModules), getApkOptimizations(appBundle.getBundleConfig()));
: shardedApksFacade.generateSplits(modulesToFuse(allModules), apkOptimizations);
}
private ImmutableList<ModuleSplit> generateAssetSlices(AppBundle appBundle) {
ApkGenerationConfiguration assetSlicesGenerationConfiguration =
getAssetSliceGenerationConfiguration(appBundle.getBundleConfig());
getAssetSliceGenerationConfiguration();
AssetSlicesGenerator assetSlicesGenerator =
new AssetSlicesGenerator(
appBundle,
......@@ -263,7 +259,7 @@ public final class BuildApksManager {
/* modulesToFuse= */ modulesToFuse.stream()
.map(BundleModule::getName)
.collect(toImmutableSet()),
getSystemApkOptimizations(appBundle.getBundleConfig()));
getSystemApkOptimizations());
}
private static void checkDeviceCompatibilityWithBundle(
......@@ -273,12 +269,17 @@ public final class BuildApksManager {
}
private ApkSetBuilder createApkSetBuilder(Path tempDir) {
if (!command.getCreateApkSetArchive()) {
return ApkSetBuilderFactory.createApkSetWithoutArchiveBuilder(
splitApkSerializer, standaloneApkSerializer, command.getOutputFile());
switch (command.getOutputFormat()) {
case APK_SET:
return ApkSetBuilderFactory.createApkSetBuilder(
splitApkSerializer, standaloneApkSerializer, tempDir);
case DIRECTORY:
return ApkSetBuilderFactory.createApkSetWithoutArchiveBuilder(
splitApkSerializer, standaloneApkSerializer, command.getOutputFile());
}
return ApkSetBuilderFactory.createApkSetBuilder(
splitApkSerializer, standaloneApkSerializer, tempDir);
throw InvalidCommandException.builder()
.withInternalMessage("Unsupported output format '%s'.", command.getOutputFormat())
.build();
}
private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfiguration(
......@@ -286,16 +287,12 @@ public final class BuildApksManager {
BundleConfig bundleConfig = appBundle.getBundleConfig();
Version bundleToolVersion = Version.of(bundleConfig.getBundletool().getVersion());
ApkOptimizations apkOptimizations = getApkOptimizations(bundleConfig);
ApkGenerationConfiguration.Builder apkGenerationConfiguration =
ApkGenerationConfiguration.builder()
.setOptimizationDimensions(apkOptimizations.getSplitDimensions());
boolean enableNativeLibraryCompressionSplitter =
apkOptimizations.getUncompressNativeLibraries();
apkGenerationConfiguration.setEnableNativeLibraryCompressionSplitter(
enableNativeLibraryCompressionSplitter);
apkGenerationConfiguration.setEnableUncompressedNativeLibraries(
apkOptimizations.getUncompressNativeLibraries());
apkGenerationConfiguration.setInstallableOnExternalStorage(
appBundle
......@@ -312,7 +309,7 @@ public final class BuildApksManager {
apkGenerationConfiguration.setMasterPinnedResourceNames(
appBundle.getMasterPinnedResourceNames());
apkGenerationConfiguration.setSuffixStrippings(getSuffixStrippings(bundleConfig));
apkGenerationConfiguration.setSuffixStrippings(apkOptimizations.getSuffixStrippings());
command
.getSigningConfiguration()
......@@ -322,13 +319,10 @@ public final class BuildApksManager {
return apkGenerationConfiguration;
}
private static ApkGenerationConfiguration getAssetSliceGenerationConfiguration(
BundleConfig bundleConfig) {
ApkOptimizations apkOptimizations = ApkOptimizations.getOptimizationsForAssetSlices();
private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() {
return ApkGenerationConfiguration.builder()
.setOptimizationDimensions(apkOptimizations.getSplitDimensions())
.setSuffixStrippings(getSuffixStrippings(bundleConfig))
.setOptimizationDimensions(apkOptimizations.getSplitDimensionsForAssetModules())
.setSuffixStrippings(apkOptimizations.getSuffixStrippings())
.build();
}
......@@ -336,19 +330,9 @@ public final class BuildApksManager {
return modules.stream().filter(BundleModule::isIncludedInFusing).collect(toImmutableList());
}
private static ImmutableMap<OptimizationDimension, SuffixStripping> getSuffixStrippings(
BundleConfig bundleConfig) {
return OptimizationsMerger.getSuffixStrippings(
bundleConfig.getOptimizations().getSplitsConfig().getSplitDimensionList());
}
private ApkOptimizations getApkOptimizations(BundleConfig bundleConfig) {
return optimizationsMerger.mergeWithDefaults(bundleConfig, command.getOptimizationDimensions());
}
private ApkOptimizations getSystemApkOptimizations(BundleConfig bundleConfig) {
private ApkOptimizations getSystemApkOptimizations() {
ImmutableSet<SystemApkOption> systemApkOptions = command.getSystemApkOptions();
return getApkOptimizations(bundleConfig).toBuilder()
return apkOptimizations.toBuilder()
.setUncompressNativeLibraries(
systemApkOptions.contains(SystemApkOption.UNCOMPRESSED_NATIVE_LIBRARIES))
.setUncompressDexFiles(systemApkOptions.contains(SystemApkOption.UNCOMPRESSED_DEX_FILES))
......@@ -533,7 +517,7 @@ public final class BuildApksManager {
if (appBundle.isAssetOnly()) {
return false;
}
return apkBuildMode.isAnySystemMode();
return apkBuildMode.equals(SYSTEM);
}
public boolean generateAssetSlices() {
......
......@@ -15,10 +15,15 @@
*/
package com.android.tools.build.bundletool.commands;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.android.tools.build.bundletool.io.TempDirectory;
import com.android.tools.build.bundletool.io.ZipReader;
import com.android.tools.build.bundletool.model.AppBundle;
import dagger.BindsInstance;
import dagger.Component;
import java.lang.annotation.Retention;
import javax.inject.Qualifier;
/** Dagger component to create a {@link BuildApksManager}. */
@CommandScoped
......@@ -39,5 +44,19 @@ public interface BuildApksManagerComponent {
@BindsInstance
Builder setAppBundle(AppBundle appBundle);
@BindsInstance
Builder setZipReader(ZipReader zipReader);
@BindsInstance
Builder setUseBundleCompression(@UseBundleCompression boolean useBundleCompression);
}
/**
* Qualifying annotation a {@code boolean} on whether entries in the APKs can be serialized with
* the same compression as the entries in the App Bundle.
*/
@Qualifier
@Retention(RUNTIME)
public @interface UseBundleCompression {}
}
......@@ -15,6 +15,7 @@
*/
package com.android.tools.build.bundletool.commands;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.android.bundle.Config.BundleConfig;
......@@ -22,6 +23,7 @@ import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode;
import com.android.tools.build.bundletool.device.AdbServer;
import com.android.tools.build.bundletool.device.DeviceAnalyzer;
import com.android.tools.build.bundletool.io.ApkSerializerModule;
import com.android.tools.build.bundletool.model.ApkListener;
import com.android.tools.build.bundletool.model.ApkModifier;
import com.android.tools.build.bundletool.model.SigningConfiguration;
......@@ -37,7 +39,13 @@ import java.util.Optional;
import javax.inject.Qualifier;
/** Dagger module for the build-apks command. */
@Module(includes = {BundleConfigModule.class, BundletoolModule.class, AppBundleModule.class})
@Module(
includes = {
BundleConfigModule.class,
BundletoolModule.class,
AppBundleModule.class,
ApkSerializerModule.class
})
public final class BuildApksModule {
@CommandScoped
......@@ -109,15 +117,22 @@ public final class BuildApksModule {
@CommandScoped
@Provides
static Optional<DeviceSpec> provideDeviceSpec(BuildApksCommand command) {
Optional<DeviceSpec> deviceSpec = command.getDeviceSpec();
if (command.getGenerateOnlyForConnectedDevice()) {
AdbServer adbServer = command.getAdbServer().get();
adbServer.init(command.getAdbPath().get());
DeviceSpec connectedDeviceSpec =
new DeviceAnalyzer(adbServer).getDeviceSpec(command.getDeviceId());
return Optional.of(connectedDeviceSpec);
deviceSpec = Optional.of(new DeviceAnalyzer(adbServer).getDeviceSpec(command.getDeviceId()));
}
return command.getDeviceSpec();
if (command.getDeviceTier().isPresent()) {
// --device-tier can only be specified along with --device-spec or --connected-device, so
// deviceSpec should always be present in this case.
checkState(deviceSpec.isPresent(), "Device tier specified but no device was provided.");
deviceSpec =
deviceSpec.map(
spec -> spec.toBuilder().setDeviceTier(command.getDeviceTier().get()).build());
}
return deviceSpec;
}
@CommandScoped
......
......@@ -16,11 +16,7 @@
package com.android.tools.build.bundletool.commands;
import com.android.bundle.Config.BundleConfig;
import com.android.bundle.Config.SuffixStripping;
import com.android.tools.build.bundletool.model.OptimizationDimension;
import com.android.tools.build.bundletool.model.version.Version;
import com.android.tools.build.bundletool.optimizations.OptimizationsMerger;
import com.google.common.collect.ImmutableMap;
import dagger.Module;
import dagger.Provides;
......@@ -34,13 +30,5 @@ public final class BundleConfigModule {
return Version.of(bundleConfig.getBundletool().getVersion());
}
@CommandScoped
@Provides
static ImmutableMap<OptimizationDimension, SuffixStripping>
provideSuffixStrippingPerOptimizationDimension(BundleConfig bundleConfig) {
return OptimizationsMerger.getSuffixStrippings(
bundleConfig.getOptimizations().getSplitsConfig().getSplitDimensionList());
}
private BundleConfigModule() {}
}
......@@ -22,18 +22,21 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.android.bundle.Commands.AssetModuleMetadata;
import com.android.bundle.Commands.AssetSliceSet;
import com.android.bundle.Commands.BuildApksResult;
import com.android.bundle.Commands.ExtractApksResult;
import com.android.bundle.Commands.ExtractedApk;
import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription;
import com.android.tools.build.bundletool.device.ApkMatcher;
import com.android.tools.build.bundletool.device.ApkMatcher.GeneratedApk;
import com.android.tools.build.bundletool.device.DeviceSpecParser;
import com.android.tools.build.bundletool.flags.Flag;
import com.android.tools.build.bundletool.flags.ParsedFlags;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException;
import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException;
import com.android.tools.build.bundletool.model.utils.FileNames;
......@@ -44,6 +47,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.protobuf.util.JsonFormat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
......@@ -66,11 +70,14 @@ public abstract class ExtractApksCommand {
public static final String COMMAND_NAME = "extract-apks";
static final String ALL_MODULES_SHORTCUT = "_ALL_";
private static final String METADATA_FILE = "metadata.json";
private static final Flag<Path> APKS_ARCHIVE_FILE_FLAG = Flag.path("apks");
private static final Flag<Path> DEVICE_SPEC_FLAG = Flag.path("device-spec");
private static final Flag<Path> OUTPUT_DIRECTORY = Flag.path("output-dir");
private static final Flag<ImmutableSet<String>> MODULES_FLAG = Flag.stringSet("modules");
private static final Flag<Boolean> INSTANT_FLAG = Flag.booleanFlag("instant");
private static final Flag<Boolean> INCLUDE_METADATA_FLAG = Flag.booleanFlag("include-metadata");
public abstract Path getApksArchivePath();
......@@ -83,10 +90,13 @@ public abstract class ExtractApksCommand {
/** Gets whether instant APKs should be extracted. */
public abstract boolean getInstant();
public abstract boolean getIncludeMetadata();
public static Builder builder() {
return new AutoValue_ExtractApksCommand.Builder()
.setInstant(false);
.setInstant(false)
.setIncludeMetadata(false);
}
/** Builder for the {@link ExtractApksCommand}. */
......@@ -112,14 +122,16 @@ public abstract class ExtractApksCommand {
*/
public abstract Builder setInstant(boolean instant);
public abstract Builder setIncludeMetadata(boolean outputMetadata);
abstract ExtractApksCommand autoBuild();
/**
* Builds the command
*
* @throws ValidationException if the device spec is invalid. See {@link
* DeviceSpecParser#validateDeviceSpec}
* @throws com.android.tools.build.bundletool.model.exceptions.InvalidDeviceSpecException if the
* device spec is invalid. See {@link DeviceSpecParser#validateDeviceSpec}
*/
public ExtractApksCommand build() {
ExtractApksCommand command = autoBuild();
......@@ -136,6 +148,7 @@ public abstract class ExtractApksCommand {
Optional<Path> outputDirectory = OUTPUT_DIRECTORY.getValue(flags);
Optional<ImmutableSet<String>> modules = MODULES_FLAG.getValue(flags);
Optional<Boolean> instant = INSTANT_FLAG.getValue(flags);
Optional<Boolean> includeMetadata = INCLUDE_METADATA_FLAG.getValue(flags);
flags.checkNoUnknownFlags();
ExtractApksCommand.Builder command = builder();
......@@ -152,6 +165,7 @@ public abstract class ExtractApksCommand {
modules.ifPresent(command::setModules);
instant.ifPresent(command::setInstant);
includeMetadata.ifPresent(command::setIncludeMetadata);
return command.build();
......@@ -170,9 +184,9 @@ public abstract class ExtractApksCommand {
getModules().map(modules -> resolveRequestedModules(modules, toc));
ApkMatcher apkMatcher = new ApkMatcher(getDeviceSpec(), requestedModuleNames, getInstant());
ImmutableList<ZipPath> matchedApks = apkMatcher.getMatchingApks(toc);
ImmutableList<GeneratedApk> generatedApks = apkMatcher.getMatchingApks(toc);
if (matchedApks.isEmpty()) {
if (generatedApks.isEmpty()) {
throw IncompatibleDeviceException.builder()
.withUserMessage("No compatible APKs found for the device.")
.build();
......@@ -180,11 +194,11 @@ public abstract class ExtractApksCommand {
if (Files.isDirectory(getApksArchivePath())) {
return matchedApks.stream()
.map(matchedApk -> getApksArchivePath().resolve(matchedApk.toString()))
return generatedApks.stream()
.map(matchedApk -> getApksArchivePath().resolve(matchedApk.getPath().toString()))
.collect(toImmutableList());
} else {
return extractMatchedApksFromApksArchive(matchedApks);
return extractMatchedApksFromApksArchive(generatedApks);
}
}
......@@ -221,7 +235,7 @@ public abstract class ExtractApksCommand {
}
private ImmutableList<Path> extractMatchedApksFromApksArchive(
ImmutableList<ZipPath> matchedApkPaths) {
ImmutableList<GeneratedApk> generatedApks) {
Path outputDirectoryPath =
getOutputDirectory().orElseGet(ExtractApksCommand::createTempDirectory);
......@@ -236,10 +250,11 @@ public abstract class ExtractApksCommand {
ImmutableList.Builder<Path> builder = ImmutableList.builder();
try (ZipFile apksArchive = new ZipFile(getApksArchivePath().toFile())) {
for (ZipPath matchedApk : matchedApkPaths) {
ZipEntry entry = apksArchive.getEntry(matchedApk.toString());
for (GeneratedApk matchedApk : generatedApks) {
ZipEntry entry = apksArchive.getEntry(matchedApk.getPath().toString());
checkNotNull(entry);
Path extractedApkPath = outputDirectoryPath.resolve(matchedApk.getFileName().toString());
Path extractedApkPath =
outputDirectoryPath.resolve(matchedApk.getPath().getFileName().toString());
try (InputStream inputStream = apksArchive.getInputStream(entry);
OutputStream outputApk = Files.newOutputStream(extractedApkPath)) {
ByteStreams.copy(inputStream, outputApk);
......@@ -249,6 +264,9 @@ public abstract class ExtractApksCommand {
String.format("Error while extracting APK '%s' from the APK Set.", matchedApk), e);
}
}
if (getIncludeMetadata()) {
produceCommandMetadata(generatedApks, outputDirectoryPath);
}
} catch (IOException e) {
throw new UncheckedIOException(
String.format("Error while processing the APK Set archive '%s'.", getApksArchivePath()),
......@@ -259,6 +277,28 @@ public abstract class ExtractApksCommand {
return builder.build();
}
private static void produceCommandMetadata(
ImmutableList<GeneratedApk> generatedApks, Path outputDir) {
ImmutableList<ExtractedApk> apks =
generatedApks.stream()
.map(
apk ->
ExtractedApk.newBuilder()
.setPath(apk.getPath().getFileName().toString())
.setModuleName(apk.getModuleName())
.setDeliveryType(apk.getDeliveryType())
.build())
.collect(toImmutableList());
try {
JsonFormat.Printer printer = JsonFormat.printer();
String metadata = printer.print(ExtractApksResult.newBuilder().addAllApks(apks).build());
Files.write(outputDir.resolve(METADATA_FILE), metadata.getBytes(UTF_8));
} catch (IOException e) {
throw new UncheckedIOException("Error while writing metadata.json.", e);
}
}
private static Path createTempDirectory() {
try {
return Files.createTempDirectory("bundletool-extracted-apks");
......@@ -321,6 +361,14 @@ public abstract class ExtractApksCommand {
"When set, APKs of the instant modules will be extracted instead of the "
+ "installable APKs.")
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(INCLUDE_METADATA_FLAG.getName())
.setOptional(true)
.setDescription(
"When set, metadata.json will be produced to the output directory with"
+ " description about extracted APKs.")
.build())
.bui