| // 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 com.google.common.base.Joiner; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ComparisonChain; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Ordering; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.TreeSet; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A parsed set of configuration filters for a split flag or an output filename. |
| * |
| * <p>The natural ordering of this class sorts by number of configurations, then by highest required |
| * API version, if any, then by other specifiers (case-insensitive), with ties broken by the |
| * filename or split flag originally used to create the instance (case-sensitive). |
| * |
| * <p>This has the following useful property:<br> |
| * Given two sets of {@link SplitConfigurationFilter}s, one from the input split flags, and one from |
| * aapt's outputs... Each member of the output set can be matched to the greatest member of the |
| * input set for which {@code input.matchesFilterFromFilename(output)} is true. |
| */ |
| final class SplitConfigurationFilter implements Comparable<SplitConfigurationFilter> { |
| |
| /** |
| * Finds a mapping from filename suffixes to the split flags which could have spawned them. |
| * |
| * @param filenames The suffixes of the original apk filenames output by aapt, not including the |
| * underscore used to set it off from the base filename or the base filename itself. |
| * @param splitFlags The split flags originally passed to aapt. |
| * @return A map whose keys are the filenames from {@code filenames} and whose values are |
| * predictable filenames based on the split flags - that is, the commas present in the input |
| * have been replaced with underscores. |
| * @throws UnrecognizedSplitException if any of the inputs are unused or could not be matched |
| */ |
| static Map<String, String> mapFilenamesToSplitFlags( |
| Iterable<String> filenames, Iterable<String> splitFlags) throws UnrecognizedSplitsException { |
| TreeSet<SplitConfigurationFilter> filenameFilters = new TreeSet<>(); |
| for (String filename : filenames) { |
| filenameFilters.add(SplitConfigurationFilter.fromFilenameSuffix(filename)); |
| } |
| TreeSet<SplitConfigurationFilter> flagFilters = new TreeSet<>(); |
| for (String splitFlag : splitFlags) { |
| flagFilters.add(SplitConfigurationFilter.fromSplitFlag(splitFlag)); |
| } |
| ImmutableMap.Builder<String, String> result = ImmutableMap.builder(); |
| List<String> unidentifiedFilenames = new ArrayList<>(); |
| for (SplitConfigurationFilter filenameFilter : filenameFilters) { |
| Optional<SplitConfigurationFilter> matched = |
| Iterables.tryFind(flagFilters, new MatchesFilterFromFilename(filenameFilter)); |
| if (matched.isPresent()) { |
| result.put(filenameFilter.filename, matched.get().filename); |
| flagFilters.remove(matched.get()); |
| } else { |
| unidentifiedFilenames.add(filenameFilter.filename); |
| } |
| } |
| if (!(unidentifiedFilenames.isEmpty() && flagFilters.isEmpty())) { |
| ImmutableList.Builder<String> unidentifiedFlags = ImmutableList.builder(); |
| for (SplitConfigurationFilter flagFilter : flagFilters) { |
| unidentifiedFlags.add(flagFilter.filename); |
| } |
| throw new UnrecognizedSplitsException( |
| unidentifiedFlags.build(), unidentifiedFilenames, result.build()); |
| } |
| return result.build(); |
| } |
| |
| /** |
| * Exception thrown when mapFilenamesToSplitFlags fails to find matches for all elements of both |
| * input sets. |
| */ |
| static final class UnrecognizedSplitsException extends Exception { |
| private final ImmutableList<String> unidentifiedSplits; |
| private final ImmutableList<String> unidentifiedFilenames; |
| private final ImmutableMap<String, String> identifiedSplits; |
| |
| UnrecognizedSplitsException( |
| Iterable<String> unidentifiedSplits, |
| Iterable<String> unidentifiedFilenames, |
| Map<String, String> identifiedSplits) { |
| super( |
| "Could not find matching filenames for these split flags:\n" |
| + Joiner.on("\n").join(unidentifiedSplits) |
| + "\nnor matching split flags for these filenames:\n" |
| + Joiner.on(", ").join(unidentifiedFilenames) |
| + "\nFound these (filename => split flag) matches though:\n" |
| + Joiner.on("\n").withKeyValueSeparator(" => ").join(identifiedSplits)); |
| this.unidentifiedSplits = ImmutableList.copyOf(unidentifiedSplits); |
| this.unidentifiedFilenames = ImmutableList.copyOf(unidentifiedFilenames); |
| this.identifiedSplits = ImmutableMap.copyOf(identifiedSplits); |
| } |
| |
| /** Returns the list of split flags which did not find a match. */ |
| ImmutableList<String> getUnidentifiedSplits() { |
| return unidentifiedSplits; |
| } |
| |
| /** Returns the list of filename suffixes which did not find a match. */ |
| ImmutableList<String> getUnidentifiedFilenames() { |
| return unidentifiedFilenames; |
| } |
| |
| /** Returns the mapping from filename suffix to split flag for splits that did match. */ |
| ImmutableMap<String, String> getIdentifiedSplits() { |
| return identifiedSplits; |
| } |
| } |
| |
| /** Generates a SplitConfigurationFilter from a split flag. */ |
| static SplitConfigurationFilter fromSplitFlag(String flag) { |
| return SplitConfigurationFilter.fromFilenameSuffix(flag.replace(',', '_')); |
| } |
| |
| /** Generates a SplitConfigurationFilter from the suffix of a split generated by aapt. */ |
| static SplitConfigurationFilter fromFilenameSuffix(String suffix) { |
| ImmutableSortedSet.Builder<ResourceConfiguration> configs = ImmutableSortedSet.reverseOrder(); |
| for (String configuration : Splitter.on('_').split(suffix)) { |
| configs.add(ResourceConfiguration.fromString(configuration)); |
| } |
| return new SplitConfigurationFilter(suffix, configs.build()); |
| } |
| |
| /** |
| * The suffix to be appended to the output package for this split configuration. |
| * |
| * <p>When created with {@link fromFilenameSuffix}, this will be the original filename from aapt; |
| * when created with {@link fromSplitFlag}, this will be the filename to rename to. |
| */ |
| private final String filename; |
| |
| /** |
| * A set of resource configurations which will be included in this split, sorted so that the |
| * configs with the highest API versions come first. |
| * |
| * <p>It's okay for this to collapse duplicates, because aapt forbids duplicate resource |
| * configurations across all splits in the same invocation anyway. |
| */ |
| private final ImmutableSortedSet<ResourceConfiguration> configs; |
| |
| private SplitConfigurationFilter( |
| String filename, ImmutableSortedSet<ResourceConfiguration> configs) { |
| this.filename = filename; |
| this.configs = configs; |
| } |
| |
| /** |
| * Checks if the {@code other} split configuration filter could have been produced as a filename |
| * by aapt based on this configuration filter being passed as a split flag. |
| * |
| * <p>This means that there must be a one-to-one mapping from each configuration in this filter to |
| * a configuration in the {@code other} filter such that the non-API-version specifiers of the two |
| * configurations match and the API version specifier of the {@code other} filter's configuration |
| * is greater than or equal to the API version specifier of this filter's configuration. |
| * |
| * <p>Order of whole configurations doesn't matter, as aapt will reorder the configurations |
| * according to complicated internal logic (yes, logic even more complicated than this!). |
| * |
| * <p>Care is needed with API version specifiers because aapt may add or change minimum API |
| * version specifiers to configurations according to whether they had specifiers which are only |
| * supported in certain versions of Android. It will only ever increase the minimum version or |
| * leave it the same. |
| * |
| * <p>The other (non-wildcard) specifiers should be case-insensitive identical, including order; |
| * aapt will not allow parts of a single configuration to be parsed out of order. |
| * |
| * @see ResourceConfiguration#matchesConfigurationFromFilename(ResourceConfiguration) |
| */ |
| boolean matchesFilterFromFilename(SplitConfigurationFilter filenameFilter) { |
| if (filenameFilter.configs.size() != this.configs.size()) { |
| return false; |
| } |
| |
| List<ResourceConfiguration> unmatchedConfigs = new ArrayList<>(this.configs); |
| for (ResourceConfiguration filenameConfig : filenameFilter.configs) { |
| Optional<ResourceConfiguration> matched = |
| Iterables.tryFind( |
| unmatchedConfigs, |
| new ResourceConfiguration.MatchesConfigurationFromFilename(filenameConfig)); |
| if (!matched.isPresent()) { |
| return false; |
| } |
| unmatchedConfigs.remove(matched.get()); |
| } |
| return true; |
| } |
| |
| static final class MatchesFilterFromFilename implements Predicate<SplitConfigurationFilter> { |
| private final SplitConfigurationFilter filenameFilter; |
| |
| MatchesFilterFromFilename(SplitConfigurationFilter filenameFilter) { |
| this.filenameFilter = filenameFilter; |
| } |
| |
| @Override |
| public boolean apply(SplitConfigurationFilter flagFilter) { |
| return flagFilter.matchesFilterFromFilename(filenameFilter); |
| } |
| } |
| |
| private static final Ordering<Iterable<ResourceConfiguration>> CONFIG_LEXICOGRAPHICAL = |
| Ordering.natural().lexicographical(); |
| |
| @Override |
| public int compareTo(SplitConfigurationFilter other) { |
| return ComparisonChain.start() |
| .compare(this.configs.size(), other.configs.size()) |
| .compare(this.configs, other.configs, CONFIG_LEXICOGRAPHICAL) |
| .compare(this.filename, other.filename) |
| .result(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(configs, filename); |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (object instanceof SplitConfigurationFilter) { |
| SplitConfigurationFilter other = (SplitConfigurationFilter) object; |
| // the configs are derived from the filename, so we can be assured they are equal if the |
| // filenames are. |
| return Objects.equals(this.filename, other.filename); |
| } |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return "SplitConfigurationFilter{" + filename + "}"; |
| } |
| |
| /** |
| * An individual set of configuration specifiers, for the purposes of split name parsing. |
| * |
| * <p>The natural ordering of this class sorts by required API version, if any, then by other |
| * specifiers. |
| * |
| * <p>This has the following useful property:<br> |
| * Given two sets of {@link ResourceConfiguration}s, one from an input split flag, and one from |
| * aapt's output... Each member of the output set can be matched to the greatest member of the |
| * input set for which {@code input.matchesConfigurationFromFilename(output)} is true. |
| */ |
| static final class ResourceConfiguration implements Comparable<ResourceConfiguration> { |
| /** |
| * Pattern to match wildcard parts ("any"), which can be safely ignored - aapt drops them. |
| * |
| * <p>Matches an 'any' part and the dash following it, or for an 'any' part which is the last |
| * specifier, the dash preceding it. In the former case, it must be a full part - that is, |
| * preceded by the beginning of the string or a dash, which will not be consumed. |
| */ |
| private static final Pattern WILDCARD_SPECIFIER = Pattern.compile("(?<=^|-)any(?:-|$)|-any$"); |
| /** |
| * Pattern to match the API version and capture the version number. |
| * |
| * <p>It must always be the last specifier in a config, although it may also be the first if |
| * there are no other specifiers. |
| */ |
| private static final Pattern API_VERSION = Pattern.compile("(?:-|^)v(\\d+)$"); |
| |
| /** Parses a resource configuration into a form that can be compared to other configurations. */ |
| static ResourceConfiguration fromString(String text) { |
| // Case is ignored for resource configurations (aapt lowercases internally), |
| // and wildcards can be dropped. |
| String cleanSpecifiers = |
| WILDCARD_SPECIFIER.matcher(text.toLowerCase(Locale.ENGLISH)).replaceAll(""); |
| Matcher apiVersionMatcher = API_VERSION.matcher(cleanSpecifiers); |
| if (apiVersionMatcher.find()) { |
| return new ResourceConfiguration( |
| cleanSpecifiers.substring(0, apiVersionMatcher.start()), |
| Integer.parseInt(apiVersionMatcher.group(1))); |
| } else { |
| return new ResourceConfiguration(cleanSpecifiers, 0); |
| } |
| } |
| |
| /** The specifiers for this resource configuration, besides API version, in lowercase. */ |
| private final String specifiers; |
| |
| /** The API version, or 0 to indicate that no API version was present in the original config. */ |
| private final int apiVersion; |
| |
| private ResourceConfiguration(String specifiers, int apiVersion) { |
| this.specifiers = specifiers; |
| this.apiVersion = apiVersion; |
| } |
| |
| /** |
| * Checks that the {@code other} configuration could be a filename generated from this one. |
| * |
| * @see SplitConfigurationFilter#matchesFilterFromFilename(SplitConfigurationFilter) |
| */ |
| boolean matchesConfigurationFromFilename(ResourceConfiguration other) { |
| return Objects.equals(other.specifiers, this.specifiers) |
| && other.apiVersion >= this.apiVersion; |
| } |
| |
| static final class MatchesConfigurationFromFilename |
| implements Predicate<ResourceConfiguration> { |
| private final ResourceConfiguration filenameConfig; |
| |
| MatchesConfigurationFromFilename(ResourceConfiguration filenameConfig) { |
| this.filenameConfig = filenameConfig; |
| } |
| |
| @Override |
| public boolean apply(ResourceConfiguration flagConfig) { |
| return flagConfig.matchesConfigurationFromFilename(filenameConfig); |
| } |
| } |
| |
| @Override |
| public int compareTo(ResourceConfiguration other) { |
| return ComparisonChain.start() |
| .compare(this.apiVersion, other.apiVersion) |
| .compare(this.specifiers, other.specifiers) |
| .result(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(specifiers, apiVersion); |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (object instanceof ResourceConfiguration) { |
| ResourceConfiguration other = (ResourceConfiguration) object; |
| return Objects.equals(this.specifiers, other.specifiers) |
| && this.apiVersion == other.apiVersion; |
| } |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return "ResourceConfiguration{" + specifiers + "-v" + Integer.toString(apiVersion) + "}"; |
| } |
| } |
| } |