aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java176
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BUILD16
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/Converters.java72
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java147
4 files changed, 400 insertions, 11 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
index fe90bda188..714970b75f 100644
--- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
@@ -15,10 +15,13 @@ package com.google.devtools.build.android;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Function;
import com.google.common.base.Joiner;
-import com.google.common.base.Throwables;
+import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.FullRevisionConverter;
@@ -46,9 +49,12 @@ import com.android.ide.common.res2.ResourceMerger;
import com.android.ide.common.res2.ResourceSet;
import com.android.manifmerger.ManifestMerger2;
import com.android.manifmerger.ManifestMerger2.Invoker;
+import com.android.manifmerger.ManifestMerger2.Invoker.Feature;
import com.android.manifmerger.ManifestMerger2.MergeFailureException;
+import com.android.manifmerger.ManifestMerger2.MergeType;
import com.android.manifmerger.ManifestMerger2.SystemProperty;
import com.android.manifmerger.MergingReport;
+import com.android.manifmerger.PlaceholderHandler;
import com.android.manifmerger.XmlDocument;
import com.android.sdklib.repository.FullRevision;
import com.android.utils.StdLogger;
@@ -59,7 +65,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -67,8 +72,12 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -77,6 +86,16 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLEventWriter;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.Attribute;
+import javax.xml.stream.events.StartElement;
+import javax.xml.stream.events.XMLEvent;
/**
* Provides a wrapper around the AOSP build tools for resource processing.
@@ -189,6 +208,17 @@ public class AndroidResourceProcessor {
}
}
+ private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps.toMap(
+ Arrays.asList(SystemProperty.values()), new Function<SystemProperty, String>() {
+ @Override public String apply(SystemProperty property) {
+ if (property == SystemProperty.PACKAGE) {
+ return "applicationId";
+ } else {
+ return property.toCamelCase();
+ }
+ }
+ });
+
private static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}");
private final StdLogger stdLogger;
@@ -209,8 +239,8 @@ public class AndroidResourceProcessor {
if (Files.exists(source)) {
if (staticIds) {
String contents = HEX_REGEX.matcher(Joiner.on("\n").join(
- Files.readAllLines(source, StandardCharsets.UTF_8))).replaceAll("0x1");
- Files.write(rOutput, contents.getBytes(StandardCharsets.UTF_8));
+ Files.readAllLines(source, UTF_8))).replaceAll("0x1");
+ Files.write(rOutput, contents.getBytes(UTF_8));
} else {
Files.copy(source, rOutput);
}
@@ -487,7 +517,7 @@ public class AndroidResourceProcessor {
}
} catch (
IOException | SAXException | ParserConfigurationException | MergeFailureException e) {
- Throwables.propagate(e);
+ throw new RuntimeException(e);
}
return new MergedAndroidData(primaryData.getResourceDir(), primaryData.getAssetDir(),
processedManifest);
@@ -495,13 +525,147 @@ public class AndroidResourceProcessor {
return primaryData;
}
+ /**
+ * Merge several manifests into one and perform placeholder substitutions. This operation uses
+ * Gradle semantics.
+ *
+ * @param manifest The primary manifest of the merge.
+ * @param mergeeManifests Manifests to be merged into {@code manifest}.
+ * @param mergeType Whether the merger should operate in application or library mode.
+ * @param values A map of strings to be used as manifest placeholders and overrides. packageName
+ * is the only disallowed value and will be ignored.
+ * @param output The path to write the resultant manifest to.
+ * @return The path of the resultant manifest, either {@code output}, or {@code manifest} if no
+ * merging was required.
+ * @throws IOException if there was a problem writing the merged manifest.
+ */
+ public Path mergeManifest(
+ Path manifest,
+ List<Path> mergeeManifests,
+ MergeType mergeType,
+ Map<String, String> values,
+ Path output) throws IOException {
+ if (mergeeManifests.isEmpty() && values.isEmpty()) {
+ return manifest;
+ }
+
+ Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType);
+ if (mergeType == MergeType.APPLICATION) {
+ manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS);
+ }
+
+ // Add mergee manifests
+ for (Path mergeeManifest : mergeeManifests) {
+ manifestMerger.addLibraryManifest(mergeeManifest.toFile());
+ }
+
+ // Extract SystemProperties from the provided values.
+ Map<String, String> placeholders = new HashMap<>(values);
+ for (SystemProperty property : SystemProperty.values()) {
+ if (values.containsKey(SYSTEM_PROPERTY_NAMES.get(property))) {
+ manifestMerger.setOverride(property, values.get(SYSTEM_PROPERTY_NAMES.get(property)));
+
+ // The manifest merger does not allow explicitly specifying either applicationId or
+ // packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these
+ // placeholders to have the same value as specified by SystemProperty.PACKAGE.
+ if (property == SystemProperty.PACKAGE) {
+ placeholders.remove(PlaceholderHandler.APPLICATION_ID);
+ placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
+ }
+ }
+ }
+
+ // Add placeholders for all values.
+ // packageName is populated from either the applicationId override or from the manifest itself;
+ // it cannot be manually specified.
+ placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
+ manifestMerger.setPlaceHolderValues(placeholders);
+
+ try {
+ MergingReport mergingReport = manifestMerger.merge();
+ switch (mergingReport.getResult()) {
+ case WARNING:
+ mergingReport.log(stdLogger);
+ Files.createDirectories(output.getParent());
+ writeMergedManifest(mergingReport, output);
+ break;
+ case SUCCESS:
+ Files.createDirectories(output.getParent());
+ writeMergedManifest(mergingReport, output);
+ break;
+ case ERROR:
+ mergingReport.log(stdLogger);
+ throw new RuntimeException(mergingReport.getReportString());
+ default:
+ throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
+ }
+ } catch (
+ SAXException | ParserConfigurationException | MergeFailureException e) {
+ throw new RuntimeException(e);
+ }
+
+ return output;
+ }
+
private void writeMergedManifest(MergingReport mergingReport,
Path manifestOut) throws IOException, SAXException, ParserConfigurationException {
XmlDocument xmlDocument = mergingReport.getMergedDocument().get();
String annotatedDocument = mergingReport.getActions().blame(xmlDocument);
stdLogger.verbose(annotatedDocument);
Files.write(
- manifestOut, xmlDocument.prettyPrint().getBytes(StandardCharsets.UTF_8));
+ manifestOut, xmlDocument.prettyPrint().getBytes(UTF_8));
+ }
+
+ /**
+ * Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file.
+ *
+ * @param manifest The input manifest.
+ * @param customPackage The package to write to the manifest.
+ * @param output The output manifest to generate.
+ * @return The output manifest if generated or the input manifest if no overwriting is required.
+ */
+ /* TODO(apell): switch from custom xml parsing to Gradle merger with NO_PLACEHOLDER_REPLACEMENT
+ * set when android common is updated to version 2.5.0.
+ */
+ public Path writeManifestPackage(Path manifest, String customPackage, Path output) {
+ if (Strings.isNullOrEmpty(customPackage)) {
+ return manifest;
+ }
+ try {
+ Files.createDirectories(output.getParent());
+ XMLEventReader reader = XMLInputFactory.newInstance()
+ .createXMLEventReader(Files.newInputStream(manifest), UTF_8.name());
+ XMLEventWriter writer = XMLOutputFactory.newInstance()
+ .createXMLEventWriter(Files.newOutputStream(output), UTF_8.name());
+ XMLEventFactory eventFactory = XMLEventFactory.newInstance();
+ while (reader.hasNext()) {
+ XMLEvent event = reader.nextEvent();
+ if (event.isStartElement()
+ && event.asStartElement().getName().toString().equalsIgnoreCase("manifest")) {
+ StartElement element = event.asStartElement();
+ @SuppressWarnings("unchecked")
+ Iterator<Attribute> attributes = element.getAttributes();
+ ImmutableList.Builder<Attribute> newAttributes = ImmutableList.builder();
+ while (attributes.hasNext()) {
+ Attribute attr = attributes.next();
+ if (attr.getName().toString().equalsIgnoreCase("package")) {
+ newAttributes.add(eventFactory.createAttribute("package", customPackage));
+ } else {
+ newAttributes.add(attr);
+ }
+ }
+ writer.add(eventFactory.createStartElement(
+ element.getName(), newAttributes.build().iterator(), element.getNamespaces()));
+ } else {
+ writer.add(event);
+ }
+ }
+ writer.flush();
+ } catch (XMLStreamException | FactoryConfigurationError | IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return output;
}
/**
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD
index 9b12c628c3..8266f666ca 100644
--- a/src/tools/android/java/com/google/devtools/build/android/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -17,6 +17,14 @@ java_binary(
)
java_binary(
+ name = "AarGeneratorAction",
+ main_class = "com.google.devtools.build.android.AarGeneratorAction",
+ runtime_deps = [
+ ":android_builder_lib",
+ ],
+)
+
+java_binary(
name = "AndroidResourceProcessingAction",
main_class = "com.google.devtools.build.android.AndroidResourceProcessingAction",
runtime_deps = [
@@ -25,16 +33,16 @@ java_binary(
)
java_binary(
- name = "ResourceShrinkerAction",
- main_class = "com.google.devtools.build.android.ResourceShinkerAction",
+ name = "ManifestMergerAction",
+ main_class = "com.google.devtools.build.android.ManifestMergerAction",
runtime_deps = [
":android_builder_lib",
],
)
java_binary(
- name = "AarGeneratorAction",
- main_class = "com.google.devtools.build.android.AarGeneratorAction",
+ name = "ResourceShrinkerAction",
+ main_class = "com.google.devtools.build.android.ResourceShinkerAction",
runtime_deps = [
":android_builder_lib",
],
diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java
index fa3d56519f..54d1150f93 100644
--- a/src/tools/android/java/com/google/devtools/build/android/Converters.java
+++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java
@@ -14,12 +14,15 @@
package com.google.devtools.build.android;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.EnumConverter;
import com.google.devtools.common.options.OptionsParsingException;
import com.android.builder.core.VariantConfiguration;
import com.android.builder.core.VariantConfiguration.Type;
+import com.android.manifmerger.ManifestMerger2;
+import com.android.manifmerger.ManifestMerger2.MergeType;
import com.android.sdklib.repository.FullRevision;
import java.nio.file.FileSystems;
@@ -28,7 +31,9 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
/**
* Some convenient converters used by android actions. Note: These are specific to android actions.
@@ -167,13 +172,29 @@ public final class Converters {
}
}
+ /** Converter for {@link ManifestMerger2}.{@link MergeType}. */
+ public static class MergeTypeConverter
+ extends EnumConverter<MergeType> {
+ public MergeTypeConverter() {
+ super(MergeType.class, "merge type");
+ }
+ }
+
/**
* Validating converter for a list of Paths.
* A Path is considered valid if it resolves to a file.
*/
public static class PathListConverter implements Converter<List<Path>> {
- final PathConverter baseConverter = new PathConverter();
+ private final PathConverter baseConverter;
+
+ public PathListConverter() {
+ this(false);
+ }
+
+ protected PathListConverter(boolean mustExist) {
+ baseConverter = new PathConverter(mustExist);
+ }
@Override
public List<Path> convert(String input) throws OptionsParsingException {
@@ -192,4 +213,53 @@ public final class Converters {
}
}
+ /**
+ * Validating converter for a list of Paths. The list is considered valid if all Paths resolve to
+ * a file that exists.
+ */
+ public static class ExistingPathListConverter extends PathListConverter {
+ public ExistingPathListConverter() {
+ super(true);
+ }
+ }
+
+ /**
+ * A converter for dictionary arguments of the format key:value[,key:value]*. The keys and values
+ * may contain colons and commas as long as they are escaped with a backslash.
+ */
+ public static class StringDictionaryConverter implements Converter<Map<String, String>> {
+ @Override
+ public Map<String, String> convert(String input) throws OptionsParsingException {
+ if (input.isEmpty()) {
+ return ImmutableMap.of();
+ }
+ Map<String, String> map = new LinkedHashMap<>();
+ // Only split on comma and colon that are not escaped with a backslash
+ for (String entry : input.split("(?<!\\\\)\\,")) {
+ String[] entryFields = entry.split("(?<!\\\\)\\:", -1);
+ if (entryFields.length < 2) {
+ throw new OptionsParsingException(String.format(
+ "Dictionary entry [%s] does not contain both a key and a value.",
+ entry));
+ } else if (entryFields.length > 2) {
+ throw new OptionsParsingException(String.format(
+ "Dictionary entry [%s] contains too many fields.",
+ entry));
+ } else if (map.containsKey(entryFields[0])) {
+ throw new OptionsParsingException(String.format(
+ "Dictionary already contains the key [%s].",
+ entryFields[0]));
+ }
+ // Unescape any comma or colon that is not a key or value separator.
+ map.put(entryFields[0].replace("\\:", ":").replace("\\,", ","),
+ entryFields[1].replace("\\:", ":").replace("\\,", ","));
+ }
+ return ImmutableMap.copyOf(map);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a comma-separated list of colon-separated key value pairs";
+ }
+ }
}
diff --git a/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java b/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java
new file mode 100644
index 0000000000..4a8f0a5500
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ManifestMergerAction.java
@@ -0,0 +1,147 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.android;
+
+import static java.util.logging.Level.SEVERE;
+
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.ExistingPathListConverter;
+import com.google.devtools.build.android.Converters.MergeTypeConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.Converters.StringDictionaryConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+
+import com.android.manifmerger.ManifestMerger2.MergeType;
+import com.android.utils.StdLogger;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.FileTime;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * An action to perform manifest merging using the Gradle manifest merger.
+ *
+ * <pre>
+ * Example Usage:
+ * java/com/google/build/android/ManifestMergerAction
+ * --manifest path to primary manifest
+ * --mergeeManifests colon separated list of manifests to merge
+ * --mergeType APPLICATION|LIBRARY
+ * --manifestValues key value pairs of manifest overrides
+ * --customPackage package to write for library manifest
+ * --manifestOutput path to write output manifest
+ * </pre>
+ */
+public class ManifestMergerAction {
+ /** Flag specifications for this action. */
+ public static final class Options extends OptionsBase {
+ @Option(name = "manifest",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "input",
+ help = "Path of primary manifest.")
+ public Path manifest;
+
+ @Option(name = "mergeeManifests",
+ defaultValue = "",
+ converter = ExistingPathListConverter.class,
+ category = "input",
+ help = "A list of manifests to be merged into manifest.")
+ public List<Path> mergeeManifests;
+
+ @Option(name = "mergeType",
+ defaultValue = "APPLICATION",
+ converter = MergeTypeConverter.class,
+ category = "config",
+ help = "The type of merging to perform.")
+ public MergeType mergeType;
+
+ @Option(name = "manifestValues",
+ defaultValue = "",
+ converter = StringDictionaryConverter.class,
+ category = "config",
+ help = "A dictionary string of values to be overridden in the manifest. Any instance of "
+ + "${name} in the manifest will be replaced with the value corresponding to name in "
+ + "this dictionary. applicationId, versionCode, versionName, minSdkVersion, "
+ + "targetSdkVersion and maxSdkVersion have a dual behavior of also overriding the "
+ + "corresponding attributes of the manifest and uses-sdk tags. packageName will be "
+ + "ignored and will be set from either applicationId or the package in manifest. The "
+ + "expected format of this string is: key:value[,key:value]*. The keys and values "
+ + "may contain colons and commas as long as they are escaped with a backslash.")
+ public Map<String, String> manifestValues;
+
+ @Option(name = "customPackage",
+ defaultValue = "null",
+ category = "config",
+ help = "Custom java package to insert in the package attribute of the manifest tag.")
+ public String customPackage;
+
+ @Option(name = "manifestOutput",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path for the merged manifest.")
+ public Path manifestOutput;
+ }
+
+ private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING);
+ private static final Logger logger = Logger.getLogger(ManifestMergerAction.class.getName());
+
+ private static Options options;
+
+ public static void main(String[] args) throws Exception {
+ OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class);
+ optionsParser.parseAndExitUponError(args);
+ options = optionsParser.getOptions(Options.class);
+
+ final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger);
+
+ try {
+ Path mergedManifest;
+ if (options.mergeType == MergeType.APPLICATION) {
+ // Ignore custom package at the binary level.
+ mergedManifest = resourceProcessor.mergeManifest(
+ options.manifest,
+ options.mergeeManifests,
+ options.mergeType,
+ options.manifestValues,
+ options.manifestOutput);
+ } else {
+ // Only need to stamp custom package into the library level.
+ mergedManifest = resourceProcessor.writeManifestPackage(
+ options.manifest, options.customPackage, options.manifestOutput);
+ }
+
+ if (!mergedManifest.equals(options.manifestOutput)) {
+ Files.copy(options.manifest, options.manifestOutput, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ // Set to the epoch for caching purposes.
+ Files.setLastModifiedTime(options.manifestOutput, FileTime.fromMillis(0L));
+ } catch (IOException e) {
+ logger.log(SEVERE, "Error during merging manifests", e);
+ throw e;
+ } finally {
+ resourceProcessor.shutdown();
+ }
+ }
+}
+