diff options
Diffstat (limited to 'src')
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(); + } + } +} + |