blob: 18040ea6a6710f34d861bde1823b62d275e0ae48 [file] [log] [blame]
// 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.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<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 = 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;
}
}
}