| // Copyright 2017 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.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.android.builder.core.VariantType; |
| 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.MergingReport.MergedManifestKind; |
| import com.android.manifmerger.PlaceholderHandler; |
| import com.android.utils.Pair; |
| import com.android.utils.StdLogger; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Maps; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| 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 manifest processing oriented tools. */ |
| public class AndroidManifestProcessor { |
| |
| /** Wrapper exception for errors thrown during manifest processing. */ |
| public static class ManifestProcessingException extends RuntimeException { |
| public ManifestProcessingException(Throwable e) { |
| super(e); |
| } |
| |
| public ManifestProcessingException(String message) { |
| super(message); |
| } |
| } |
| |
| private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = |
| Maps.toMap( |
| Arrays.asList(SystemProperty.values()), |
| property -> { |
| if (property == SystemProperty.PACKAGE) { |
| return "applicationId"; |
| } else { |
| return property.toCamelCase(); |
| } |
| }); |
| |
| /** Creates a new processor with the appropriate logger. */ |
| public static AndroidManifestProcessor with(StdLogger stdLogger) { |
| return new AndroidManifestProcessor(stdLogger); |
| } |
| |
| private final StdLogger stdLogger; |
| |
| private AndroidManifestProcessor(StdLogger stdLogger) { |
| this.stdLogger = stdLogger; |
| } |
| |
| /** |
| * 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. |
| * @param logFile The path to write the merger log to. |
| * @return The path of the resultant manifest, either {@code output}, or {@code manifest} if no |
| * merging was required. |
| * @throws ManifestProcessingException if there was a problem writing the merged manifest. |
| */ |
| // TODO(corysmith): Extract manifest processing. |
| public Path mergeManifest( |
| Path manifest, |
| Map<Path, String> mergeeManifests, |
| MergeType mergeType, |
| Map<String, String> values, |
| String customPackage, |
| Path output, |
| Path logFile, |
| boolean logWarnings) |
| throws ManifestProcessingException { |
| if (mergeeManifests.isEmpty() && values.isEmpty() && Strings.isNullOrEmpty(customPackage)) { |
| return manifest; |
| } |
| |
| Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType); |
| MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; |
| if (mergeType == MergeType.APPLICATION) { |
| manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS); |
| } |
| |
| // Add mergee manifests |
| List<Pair<String, File>> libraryManifests = new ArrayList<>(); |
| for (Map.Entry<Path, String> mergeeManifest : mergeeManifests.entrySet()) { |
| libraryManifests.add(Pair.of(mergeeManifest.getValue(), mergeeManifest.getKey().toFile())); |
| } |
| manifestMerger.addLibraryManifests(libraryManifests); |
| |
| // Extract SystemProperties from the provided values. |
| Map<String, Object> placeholders = new LinkedHashMap<>(); |
| placeholders.putAll(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); |
| |
| // Ignore custom package at the binary level. |
| if (!Strings.isNullOrEmpty(customPackage) && mergeType == MergeType.LIBRARY) { |
| manifestMerger.setOverride(SystemProperty.PACKAGE, customPackage); |
| } |
| |
| try { |
| MergingReport mergingReport = manifestMerger.merge(); |
| |
| if (logFile != null) { |
| logFile.getParent().toFile().mkdirs(); |
| try (PrintStream stream = new PrintStream(logFile.toFile())) { |
| mergingReport.log(new AndroidResourceProcessor.PrintStreamLogger(stream)); |
| } |
| } |
| switch (mergingReport.getResult()) { |
| case WARNING: |
| if (logWarnings) { |
| mergingReport.log(stdLogger); |
| } |
| Files.createDirectories(output.getParent()); |
| writeMergedManifest(mergedManifestKind, mergingReport, output); |
| break; |
| case SUCCESS: |
| Files.createDirectories(output.getParent()); |
| writeMergedManifest(mergedManifestKind, mergingReport, output); |
| break; |
| case ERROR: |
| mergingReport.log(stdLogger); |
| throw new ManifestProcessingException(mergingReport.getReportString()); |
| default: |
| throw new ManifestProcessingException( |
| "Unhandled result type : " + mergingReport.getResult()); |
| } |
| } catch (MergeFailureException | IOException e) { |
| throw new ManifestProcessingException(e); |
| } |
| |
| return output; |
| } |
| |
| /** Process a manifest for a library or a binary and return the merged android data. */ |
| public MergedAndroidData processManifest( |
| VariantType variantType, |
| String customPackageForR, |
| String applicationId, |
| int versionCode, |
| String versionName, |
| MergedAndroidData primaryData, |
| Path processedManifest, |
| boolean logWarnings) { |
| |
| ManifestMerger2.MergeType mergeType = |
| variantType == VariantType.DEFAULT |
| ? ManifestMerger2.MergeType.APPLICATION |
| : ManifestMerger2.MergeType.LIBRARY; |
| |
| String newManifestPackage = |
| variantType == VariantType.DEFAULT ? applicationId : customPackageForR; |
| |
| if (versionCode != -1 || versionName != null || newManifestPackage != null) { |
| processManifest( |
| versionCode, |
| versionName, |
| primaryData.getManifest(), |
| processedManifest, |
| mergeType, |
| newManifestPackage, |
| logWarnings); |
| return new MergedAndroidData( |
| primaryData.getResourceDir(), primaryData.getAssetDir(), processedManifest); |
| } |
| return primaryData; |
| } |
| |
| /** Processes the manifest for a binary and return the manifest Path. */ |
| public Path processManifest( |
| String applicationId, |
| int versionCode, |
| String versionName, |
| Path manifest, |
| Path processedManifest, |
| boolean logWarnings) { |
| |
| if (versionCode != -1 || versionName != null || applicationId != null) { |
| processManifest( |
| versionCode, |
| versionName, |
| manifest, |
| processedManifest, |
| MergeType.APPLICATION, |
| applicationId, |
| logWarnings); |
| return processedManifest; |
| } |
| return manifest; |
| } |
| |
| /** Processes the manifest for a library and return the manifest Path. */ |
| public Path processLibraryManifest( |
| String newManifestPackage, Path manifest, Path processedManifest, boolean logWarnings) { |
| |
| if (newManifestPackage != null) { |
| processManifest( |
| /* versionCode= */ -1, |
| /* versionName= */ null, |
| manifest, |
| processedManifest, |
| MergeType.LIBRARY, |
| newManifestPackage, |
| logWarnings); |
| return processedManifest; |
| } |
| return manifest; |
| } |
| |
| private void processManifest( |
| int versionCode, |
| String versionName, |
| Path primaryManifest, |
| Path processedManifest, |
| MergeType mergeType, |
| String newManifestPackage, |
| boolean logWarnings) { |
| try { |
| Files.createDirectories(processedManifest.getParent()); |
| |
| // The generics on Invoker don't make sense, so ignore them. |
| @SuppressWarnings("unchecked") |
| Invoker<?> manifestMergerInvoker = |
| ManifestMerger2.newMerger(primaryManifest.toFile(), stdLogger, mergeType); |
| // Stamp new package |
| if (newManifestPackage != null) { |
| manifestMergerInvoker.setOverride(SystemProperty.PACKAGE, newManifestPackage); |
| } |
| // Stamp version and applicationId (if provided) into the manifest |
| if (versionCode > 0) { |
| manifestMergerInvoker.setOverride(SystemProperty.VERSION_CODE, String.valueOf(versionCode)); |
| } |
| if (versionName != null) { |
| manifestMergerInvoker.setOverride(SystemProperty.VERSION_NAME, versionName); |
| } |
| |
| MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED; |
| if (mergeType == MergeType.APPLICATION) { |
| manifestMergerInvoker.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS); |
| } |
| |
| MergingReport mergingReport = manifestMergerInvoker.merge(); |
| switch (mergingReport.getResult()) { |
| case WARNING: |
| if (logWarnings) { |
| mergingReport.log(stdLogger); |
| } |
| writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); |
| break; |
| case SUCCESS: |
| writeMergedManifest(mergedManifestKind, mergingReport, processedManifest); |
| break; |
| case ERROR: |
| mergingReport.log(stdLogger); |
| throw new ManifestProcessingException(mergingReport.getReportString()); |
| default: |
| throw new ManifestProcessingException( |
| "Unhandled result type : " + mergingReport.getResult()); |
| } |
| } catch (IOException | MergeFailureException e) { |
| throw new ManifestProcessingException(e); |
| } |
| } |
| |
| /** |
| * 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 ManifestProcessingException(e); |
| } |
| |
| return output; |
| } |
| |
| public void writeMergedManifest( |
| MergedManifestKind mergedManifestKind, MergingReport mergingReport, Path manifestOut) |
| throws ManifestProcessingException { |
| String manifestContents = mergingReport.getMergedDocument(mergedManifestKind); |
| String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME); |
| stdLogger.verbose(annotatedDocument); |
| try { |
| Files.write(manifestOut, manifestContents.getBytes(UTF_8)); |
| } catch (IOException e) { |
| throw new ManifestProcessingException(e); |
| } |
| } |
| } |