// 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.android.manifmerger.ManifestMerger2.MergeType; import com.android.utils.StdLogger; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.ExistingPathStringDictionaryConverter; 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.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Map; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * An action to perform manifest merging using the Gradle manifest merger. * *
 * 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
 * 
*/ 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", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path of primary manifest. If not passed, a dummy manifest will be generated and used as" + " the primary." ) public Path manifest; @Option( name = "mergeeManifests", defaultValue = "", converter = ExistingPathStringDictionaryConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "A dictionary of manifests, and originating target, to be merged into manifest." ) public Map mergeeManifests; @Option( name = "mergeType", defaultValue = "APPLICATION", converter = MergeTypeConverter.class, category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "The type of merging to perform." ) public MergeType mergeType; @Option( name = "manifestValues", defaultValue = "", converter = StringDictionaryConverter.class, category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, 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 manifestValues; @Option( name = "customPackage", defaultValue = "null", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, 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", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path for the merged manifest." ) public Path manifestOutput; @Option( name = "log", defaultValue = "null", category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, converter = PathConverter.class, help = "Path to where the merger log should be written." ) public Path log; } private static final String[] PERMISSION_TAGS = new String[] {"uses-permission", "uses-permission-sdk-23"}; private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING); private static final Logger logger = Logger.getLogger(ManifestMergerAction.class.getName()); private static Options options; private static Path removePermissions(Path manifest, Path outputDir) throws IOException, ParserConfigurationException, TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError, SAXException { DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = docBuilder.parse(manifest.toFile()); for (String tag : PERMISSION_TAGS) { NodeList permissions = doc.getElementsByTagName(tag); if (permissions != null) { for (int i = permissions.getLength() - 1; i >= 0; i--) { Node permission = permissions.item(i); permission.getParentNode().removeChild(permission); } } } // Write resulting manifest to the output directory, maintaining full path to prevent collisions Path output = outputDir.resolve(manifest.toString().replaceFirst("^/", "")); Files.createDirectories(output.getParent()); TransformerFactory.newInstance() .newTransformer() .transform(new DOMSource(doc), new StreamResult(output.toFile())); return output; } public static void main(String[] args) throws Exception { OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); optionsParser.enableParamsFileSupport( new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); optionsParser.parseAndExitUponError(args); options = optionsParser.getOptions(Options.class); try { Path mergedManifest; AndroidManifestProcessor manifestProcessor = AndroidManifestProcessor.with(stdLogger); // Remove uses-permission tags from mergees before the merge. Path tmp = Files.createTempDirectory("manifest_merge_tmp"); tmp.toFile().deleteOnExit(); ImmutableMap.Builder mergeeManifests = ImmutableMap.builder(); for (Map.Entry mergeeManifest : options.mergeeManifests.entrySet()) { mergeeManifests.put( removePermissions(mergeeManifest.getKey(), tmp), mergeeManifest.getValue()); } Path manifest = options.manifest; if (manifest == null) { // No primary manifest was passed. Generate a dummy primary. manifest = tmp.resolve("dummy_AndroidManifest.xml"); AndroidResourceProcessor.writeDummyManifestForAapt(manifest, options.customPackage); } mergedManifest = manifestProcessor.mergeManifest( manifest, mergeeManifests.build(), options.mergeType, options.manifestValues, options.customPackage, options.manifestOutput, options.log); if (!mergedManifest.equals(options.manifestOutput)) { // manifestProcess.mergeManifest returns the merged manifest, or, if merging was a no-op, // the original primary manifest. In the latter case, explicitly copy that primary manifest // to the expected location of the output. Files.copy(manifest, options.manifestOutput, StandardCopyOption.REPLACE_EXISTING); } } catch (AndroidManifestProcessor.ManifestProcessingException e) { System.exit(1); } catch (Exception e) { logger.log(SEVERE, "Error during merging manifests", e); throw e; } } }