|  | // 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. | 
|  | * | 
|  | * <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", | 
|  | 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<Path, String> 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<String, String> 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.builder() | 
|  | .optionsClasses(Options.class) | 
|  | .argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())) | 
|  | .build(); | 
|  | 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<Path, String> mergeeManifests = ImmutableMap.builder(); | 
|  | for (Map.Entry<Path, String> 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 = AndroidManifest.asEmpty().writeDummyManifestForAapt(tmp, 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) { | 
|  | // We special case ManifestProcessingExceptions here to indicate that this is | 
|  | // caused by a build error, not an Bazel-internal error. | 
|  | logger.log(SEVERE, "Error during merging manifests", e); | 
|  | System.exit(1); // Don't duplicate the error to the user or bubble up the exception. | 
|  | } catch (Exception e) { | 
|  | logger.log(SEVERE, "Error during merging manifests", e); | 
|  | throw e; // This is a proper internal exception, so we bubble it up. | 
|  | } | 
|  | } | 
|  | } |