提交 1146d790 编写于 作者: P Pierre Lecesne

Prepare for release 0.7.1.

上级 2f293363
......@@ -24,4 +24,4 @@ Read more about the App Bundle format and Bundletool's usage at
## Releases
Latest release: [0.7.0](https://github.com/google/bundletool/releases)
Latest release: [0.7.1](https://github.com/google/bundletool/releases)
release_version = 0.7.0
release_version = 0.7.1
......@@ -553,6 +553,7 @@ public abstract class BuildApksCommand {
FlagDescription.builder()
.setFlagName(DEVICE_SPEC_FLAG.getName())
.setExampleValue("device-spec.json")
.setOptional(true)
.setDescription(
"Path to the device spec file generated by the '%s' command. If present, "
+ "it will generate an APK Set optimized for the specified device spec.",
......
......@@ -24,6 +24,7 @@ import static com.google.common.io.MoreFiles.getNameWithoutExtension;
import com.android.bundle.Config.BundleConfig;
import com.android.bundle.Config.Bundletool;
import com.android.bundle.Files.ApexImages;
import com.android.bundle.Files.Assets;
import com.android.bundle.Files.NativeLibraries;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
......@@ -177,8 +178,10 @@ public abstract class BuildBundleCommand {
ZipBuilder bundleBuilder = new ZipBuilder();
try (Closer closer = Closer.create()) {
EntryOption[] compression =
getUncompressedBundle() ? new EntryOption[] {EntryOption.UNCOMPRESSED} : new EntryOption[0];
EntryOption[] compression =
getUncompressedBundle()
? new EntryOption[] {EntryOption.UNCOMPRESSED}
: new EntryOption[0];
// Merge in all the modules, each module into its own sub-directory.
for (Path module : getModulesPaths()) {
......@@ -189,17 +192,23 @@ public abstract class BuildBundleCommand {
bundleBuilder.copyAllContentsFromZip(moduleDir, moduleZipFile, compression);
Optional<Assets> assetsTargeting = generateAssetsTargeting(moduleZipFile);
if (assetsTargeting.isPresent()) {
bundleBuilder.addFileWithProtoContent(
moduleDir.resolve("assets.pb"), assetsTargeting.get(), compression);
}
assetsTargeting.ifPresent(
targeting ->
bundleBuilder.addFileWithProtoContent(
moduleDir.resolve("assets.pb"), targeting, compression));
Optional<NativeLibraries> nativeLibrariesTargeting =
generateNativeLibrariesTargeting(moduleZipFile);
if (nativeLibrariesTargeting.isPresent()) {
bundleBuilder.addFileWithProtoContent(
moduleDir.resolve("native.pb"), nativeLibrariesTargeting.get(), compression);
}
nativeLibrariesTargeting.ifPresent(
targeting ->
bundleBuilder.addFileWithProtoContent(
moduleDir.resolve("native.pb"), targeting, compression));
Optional<ApexImages> apexImagesTargeting = generateApexImagesTargeting(moduleZipFile);
apexImagesTargeting.ifPresent(
targeting ->
bundleBuilder.addFileWithProtoContent(
moduleDir.resolve("apex.pb"), targeting, compression));
} catch (ZipException e) {
throw CommandExecutionException.builder()
.withCause(e)
......@@ -261,7 +270,8 @@ public abstract class BuildBundleCommand {
private Optional<Assets> generateAssetsTargeting(ZipFile module) {
ImmutableList<ZipPath> assetDirectories =
ZipUtils.getFilesWithPathPrefix(module, BundleModule.ASSETS_DIRECTORY)
ZipUtils.allFileEntriesPaths(module)
.filter(path -> path.startsWith(BundleModule.ASSETS_DIRECTORY))
.filter(path -> path.getNameCount() > 1)
.map(ZipPath::getParent)
.distinct()
......@@ -278,7 +288,8 @@ public abstract class BuildBundleCommand {
// Validation ensures that files under "lib/" conform to pattern "lib/<abi-dir>/file.so".
// We extract the distinct "lib/<abi-dir>" directories.
ImmutableList<String> libAbiDirs =
ZipUtils.getFilesWithPathPrefix(module, BundleModule.LIB_DIRECTORY)
ZipUtils.allFileEntriesPaths(module)
.filter(path -> path.startsWith(BundleModule.LIB_DIRECTORY))
.filter(path -> path.getNameCount() > 2)
.map(path -> path.subpath(0, 2))
.map(ZipPath::toString)
......@@ -292,6 +303,21 @@ public abstract class BuildBundleCommand {
return Optional.of(new TargetingGenerator().generateTargetingForNativeLibraries(libAbiDirs));
}
private Optional<ApexImages> generateApexImagesTargeting(ZipFile module) {
// Validation ensures that files under "apex/" conform to the pattern
// "apex/<abi1>.<abi2>...<abiN>.img".
ImmutableList<ZipPath> apexImageFiles =
ZipUtils.allFileEntriesPaths(module)
.filter(path -> path.startsWith(BundleModule.APEX_DIRECTORY))
.collect(toImmutableList());
if (apexImageFiles.isEmpty()) {
return Optional.empty();
}
return Optional.of(new TargetingGenerator().generateTargetingForApexImages(apexImageFiles));
}
private static BundleConfig parseBundleConfigJson(Path bundleConfigJsonPath) {
BundleConfig.Builder bundleConfig = BundleConfig.newBuilder();
try (Reader bundleConfigReader = BufferedIo.reader(bundleConfigJsonPath)) {
......
......@@ -16,38 +16,26 @@
package com.android.tools.build.bundletool.commands;
import static com.android.tools.build.bundletool.utils.files.FilePreconditions.checkFileExistsAndReadable;
import static java.util.stream.Collectors.toList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.function.Function.identity;
import com.android.aapt.Resources.XmlNode;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription;
import com.android.tools.build.bundletool.exceptions.ValidationException;
import com.android.tools.build.bundletool.model.BundleModule;
import com.android.tools.build.bundletool.model.BundleModuleName;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.ResourceTableEntry;
import com.android.tools.build.bundletool.utils.flags.Flag;
import com.android.tools.build.bundletool.utils.flags.ParsedFlags;
import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoNode;
import com.android.tools.build.bundletool.xml.XPathResolver;
import com.android.tools.build.bundletool.xml.XPathResolver.XPathResult;
import com.android.tools.build.bundletool.xml.XmlNamespaceContext;
import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter;
import com.android.tools.build.bundletool.xml.XmlUtils;
import com.google.auto.value.AutoValue;
import java.io.IOException;
import java.io.InputStream;
import com.google.common.collect.ImmutableMap;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Command that prints information about a given Android App Bundle. */
@AutoValue
......@@ -58,6 +46,11 @@ public abstract class DumpCommand {
private static final Flag<Path> BUNDLE_LOCATION_FLAG = Flag.path("bundle");
private static final Flag<String> MODULE_FLAG = Flag.string("module");
private static final Flag<String> XPATH_FLAG = Flag.string("xpath");
private static final Flag<String> RESOURCE_FLAG = Flag.string("resource");
private static final Flag<Boolean> VALUES_FLAG = Flag.booleanFlag("values");
private static final Pattern RESOURCE_NAME_PATTERN =
Pattern.compile("(?<type>[^/]+)/(?<name>[^/]+)");
public abstract Path getBundlePath();
......@@ -65,14 +58,18 @@ public abstract class DumpCommand {
public abstract DumpTarget getDumpTarget();
public abstract String getModuleName();
public abstract Optional<String> getModuleName();
public abstract Optional<String> getXPathExpression();
public abstract Optional<Integer> getResourceId();
public abstract Optional<String> getResourceName();
public abstract Optional<Boolean> getPrintValues();
public static Builder builder() {
return new AutoValue_DumpCommand.Builder()
.setModuleName(BundleModuleName.BASE_MODULE_NAME)
.setOutputStream(System.out);
return new AutoValue_DumpCommand.Builder().setOutputStream(System.out);
}
/** Builder for the {@link DumpCommand}. */
......@@ -90,8 +87,27 @@ public abstract class DumpCommand {
/** Sets the module for the target of the dump. */
public abstract Builder setModuleName(String moduleName);
/** Sets the XPath expression used to extract only part of the XML file being printed. */
public abstract Builder setXPathExpression(String xPathExpression);
/**
* Sets the ID of the resource to print.
*
* <p>Mutually exclusive with {@link #setResourceName}.
*/
public abstract Builder setResourceId(int resourceId);
/**
* Sets the name of the resource to print. Must have the format "<type>/<name>", e.g.
* "drawable/icon".
*
* <p>Mutually exclusive with {@link #setResourceId}.
*/
public abstract Builder setResourceName(String resourceName);
/** Sets whether the values should also be printed when printing the resources. */
public abstract Builder setPrintValues(boolean printValues);
public abstract DumpCommand build();
}
......@@ -99,16 +115,25 @@ public abstract class DumpCommand {
DumpTarget dumpTarget = parseDumpTarget(flags);
Path bundlePath = BUNDLE_LOCATION_FLAG.getRequiredValue(flags);
String moduleName = MODULE_FLAG.getValue(flags).orElse(BundleModuleName.BASE_MODULE_NAME);
Optional<String> moduleName = MODULE_FLAG.getValue(flags);
Optional<String> xPath = XPATH_FLAG.getValue(flags);
Optional<String> resource = RESOURCE_FLAG.getValue(flags);
Optional<Boolean> printValues = VALUES_FLAG.getValue(flags);
DumpCommand.Builder dumpCommand =
DumpCommand.builder()
.setBundlePath(bundlePath)
.setDumpTarget(dumpTarget)
.setModuleName(moduleName);
DumpCommand.builder().setBundlePath(bundlePath).setDumpTarget(dumpTarget);
moduleName.ifPresent(dumpCommand::setModuleName);
xPath.ifPresent(dumpCommand::setXPathExpression);
printValues.ifPresent(dumpCommand::setPrintValues);
resource.ifPresent(
r -> {
try {
dumpCommand.setResourceId(Integer.decode(r));
} catch (NumberFormatException e) {
dumpCommand.setResourceName(r);
}
});
return dumpCommand.build();
}
......@@ -118,56 +143,41 @@ public abstract class DumpCommand {
switch (getDumpTarget()) {
case MANIFEST:
printManifest(getXPathExpression());
BundleModuleName moduleName =
BundleModuleName.create(getModuleName().orElse(BundleModuleName.BASE_MODULE_NAME));
new DumpManager(getOutputStream(), getBundlePath())
.printManifest(moduleName, getXPathExpression());
break;
}
}
private void printManifest(Optional<String> xPathExpression) {
// Extract the manifest from the bundle.
XmlProtoNode manifestProto;
try (ZipFile zipFile = new ZipFile(getBundlePath().toFile())) {
ZipPath manifestPath = ZipPath.create(getModuleName()).resolve(BundleModule.MANIFEST_PATH);
ZipEntry manifestEntry = zipFile.getEntry(manifestPath.toString());
if (manifestEntry == null) {
throw ValidationException.builder()
.withMessage(
"No manifest found for module '%s'. Does the module exist?", getModuleName())
.build();
}
try (InputStream manifestInputStream = zipFile.getInputStream(manifestEntry)) {
manifestProto = new XmlProtoNode(XmlNode.parseFrom(manifestInputStream));
}
} catch (IOException e) {
throw new UncheckedIOException("Unable to read the manifest from the bundle.", e);
}
// Convert the proto to real XML.
Document document = XmlProtoToXmlConverter.convert(manifestProto);
// Select the output.
String output;
if (xPathExpression.isPresent()) {
try {
XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new XmlNamespaceContext(manifestProto));
XPathExpression compiledXPathExpression = xPath.compile(xPathExpression.get());
XPathResult xPathResult = XPathResolver.resolve(document, compiledXPathExpression);
output = xPathResult.toString();
} catch (XPathExpressionException e) {
throw new ValidationException("Error in the XPath expression: " + xPathExpression, e);
}
} else {
output = XmlUtils.documentToString(document);
case RESOURCES:
new DumpManager(getOutputStream(), getBundlePath())
.printResources(parseResourcePredicate(), getPrintValues().orElse(false));
break;
}
// Print the output.
getOutputStream().println(output.trim());
}
private void validateInput() {
checkFileExistsAndReadable(getBundlePath());
if (getResourceId().isPresent() && getResourceName().isPresent()) {
throw new ValidationException("Cannot pass both resource ID and resource name. Pick one!");
}
if (getDumpTarget().equals(DumpTarget.RESOURCES) && getXPathExpression().isPresent()) {
throw new ValidationException("Cannot pass an XPath expression when dumping resources.");
}
if (getDumpTarget().equals(DumpTarget.RESOURCES) && getModuleName().isPresent()) {
throw new ValidationException(
"The --module flag is unnecessary as the 'dump resources' by default searches across all "
+ "modules.");
}
if (!getDumpTarget().equals(DumpTarget.RESOURCES)
&& (getResourceId().isPresent() || getResourceName().isPresent())) {
throw new ValidationException(
"The --resource flag can only be passed when dumping resources.");
}
if (!getDumpTarget().equals(DumpTarget.RESOURCES) && getPrintValues().isPresent()) {
throw new ValidationException("The --values flag can only be passed when dumping resources.");
}
}
private static DumpTarget parseDumpTarget(ParsedFlags flags) {
......@@ -176,40 +186,96 @@ public abstract class DumpCommand {
.getSubCommand()
.orElseThrow(() -> new ValidationException("Target of the dump not found."));
DumpTarget dumpTarget;
switch (subCommand) {
case "manifest":
dumpTarget = DumpTarget.MANIFEST;
break;
default:
throw ValidationException.builder()
.withMessage(
"Unrecognized dump target: '%s'. Accepted values are: %s",
subCommand,
Arrays.stream(DumpTarget.values())
.map(Enum::toString)
.map(String::toLowerCase)
.collect(toList()))
.build();
return DumpTarget.fromString(subCommand);
}
private Predicate<ResourceTableEntry> parseResourcePredicate() {
if (getResourceId().isPresent()) {
return entry -> entry.getResourceId().getFullResourceId() == getResourceId().get().intValue();
}
return dumpTarget;
if (getResourceName().isPresent()) {
String resourceName = getResourceName().get();
Matcher matcher = RESOURCE_NAME_PATTERN.matcher(resourceName);
if (!matcher.matches()) {
throw new ValidationException(
"Resource name must match the format '<type>/<name>', e.g. 'drawable/icon'.");
}
return entry ->
entry.getType().getName().equals(matcher.group("type"))
&& entry.getEntry().getName().equals(matcher.group("name"));
}
return entry -> true;
}
/** Target of the dump. */
public enum DumpTarget {
MANIFEST,
MANIFEST("manifest"),
RESOURCES("resources");
static final ImmutableMap<String, DumpTarget> SUBCOMMAND_TO_TARGET =
Arrays.stream(DumpTarget.values())
.collect(toImmutableMap(DumpTarget::toString, identity()));
private final String subCommand;
DumpTarget(String subCommand) {
this.subCommand = subCommand;
}
@Override
public String toString() {
return subCommand;
}
public static DumpTarget fromString(String subCommand) {
DumpTarget dumpTarget = SUBCOMMAND_TO_TARGET.get(subCommand);
if (dumpTarget == null) {
throw ValidationException.builder()
.withMessage(
"Unrecognized dump target: '%s'. Accepted values are: %s",
subCommand, SUBCOMMAND_TO_TARGET.keySet())
.build();
}
return dumpTarget;
}
}
public static CommandHelp help() {
Set<String> dumpTargets = DumpTarget.SUBCOMMAND_TO_TARGET.keySet();
return CommandHelp.builder()
.setCommandName(COMMAND_NAME)
.setCommandDescription(
CommandDescription.builder()
.setShortDescription(
"Prints files or extract values from the bundle in a human-readable form.")
.addAdditionalParagraph("Subcommands available: " + dumpTargets)
.addAdditionalParagraph("Examples:")
.addAdditionalParagraph(
String.format(
"1. Prints the AndroidManifest.xml of the base module:%n"
+ "$ bundletool dump manifest --bundle=/tmp/app.aab"))
.addAdditionalParagraph(
String.format(
"2. Prints the versionCode of the bundle of the base module:%n"
+ "$ bundletool dump manifest --bundle=/tmp/app.aab "
+ "--xpath=/manifest/@versionCode"))
.addAdditionalParagraph(
String.format(
"3. Prints all the resources present in the bundle:%n"
+ "$ bundletool dump resources --bundle=/tmp/app.aab"))
.addAdditionalParagraph(
"To print the manifest, one can for example run: "
+ "bundletool dump manifest --bundle=/tmp/app.aab")
String.format(
"4. Prints a resource's configs from its resource ID:%n"
+ "$ bundletool dump resources --bundle=/tmp/app.aab "
+ "--resource=0x7f0e013a"))
.addAdditionalParagraph(
String.format(
"5. Prints a resource's configs and values from its resource type & name:%n"
+ "$ bundletool dump resources --bundle=/tmp/app.aab "
+ "--resource=drawable/icon --values"))
.build())
.addFlag(
FlagDescription.builder()
......@@ -220,7 +286,9 @@ public abstract class DumpCommand {
.addFlag(
FlagDescription.builder()
.setFlagName("module")
.setDescription("Name of the module to apply the dump for. Defaults to 'base'.")
.setDescription(
"Name of the module to apply the dump for. Only applies when dumping the "
+ "manifest. Defaults to 'base'.")
.setExampleValue("base")
.setOptional(true)
.build())
......@@ -229,10 +297,29 @@ public abstract class DumpCommand {
.setFlagName("xpath")
.setDescription(
"XPath expression to extract the value of attributes from the XML file being "
+ "dumped.")
+ "dumped. Only applies when dumping the manifest.")
.setExampleValue("/manifest/@android:versionCode")
.setOptional(true)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName("resource")
.setDescription(
"Name or ID of the resource to lookup. Only applies when dumping resources. If "
+ "a resource ID is provided, it can be specified either as a decimal or "
+ "hexadecimal integer. If a resource name is provided, it must follow the "
+ "format '<type>/<name>', e.g. 'drawable/icon'")
.setExampleValue("0x7f030001")
.setOptional(true)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName("values")
.setDescription(
"When set, also prints the values of the resources. Defaults to false. "
+ "Only applies when dumping the resources.")
.setOptional(true)
.build())
.build();
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.tools.build.bundletool.commands;
import static com.android.tools.build.bundletool.utils.CollectorUtils.groupingBySortedKeys;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.android.aapt.ConfigurationOuterClass.Configuration;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.XmlNode;
import com.android.tools.build.bundletool.exceptions.ValidationException;
import com.android.tools.build.bundletool.model.BundleModule;
import com.android.tools.build.bundletool.model.BundleModuleName;
import com.android.tools.build.bundletool.model.ResourceTableEntry;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.utils.ResourcesUtils;
import com.android.tools.build.bundletool.utils.ZipUtils;
import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoNode;
import com.android.tools.build.bundletool.utils.xmlproto.XmlProtoPrintUtils;
import com.android.tools.build.bundletool.xml.XPathResolver;
import com.android.tools.build.bundletool.xml.XPathResolver.XPathResult;
import com.android.tools.build.bundletool.xml.XmlNamespaceContext;
import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter;
import com.android.tools.build.bundletool.xml.XmlUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
final class DumpManager {
private final PrintStream printStream;
private final Path bundlePath;
DumpManager(OutputStream outputStream, Path bundlePath) {
this.printStream = new PrintStream(outputStream);
this.bundlePath = bundlePath;
}
void printManifest(BundleModuleName moduleName, Optional<String> xPathExpression) {
// Extract the manifest from the bundle.
ZipPath manifestPath = ZipPath.create(moduleName.getName()).resolve(BundleModule.MANIFEST_PATH);
XmlProtoNode manifestProto =
new XmlProtoNode(extractAndParse(bundlePath, manifestPath, XmlNode::parseFrom));
// Convert the proto to real XML.
Document document = XmlProtoToXmlConverter.convert(manifestProto);