blob: 4103b800a280cd64797ee444d5325f05d58b6107 [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 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) + "}";
}
}
}