blob: 5281eacbffd207a249f69d5c470e31a95cdb6d8d [file] [log] [blame]
// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
import com.android.tools.r8.ArchiveClassFileProvider;
import com.android.tools.r8.dexsplitter.DexSplitter.FeatureJar;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;
/**
* Provides a mappings of classes to modules. The structure of the input file is as follows:
* packageOrClass:module
*
* <p>Lines with a # prefix are ignored.
*
* <p>We will do most specific matching, i.e.,
* <pre>
* com.google.foobar.*:feature2
* com.google.*:base
* </pre>
* will put everything in the com.google namespace into base, except classes in com.google.foobar
* that will go to feature2. Class based mappings takes precedence over packages (since they are
* more specific):
* <pre>
* com.google.A:feature2
* com.google.*:base
* </pre>
* Puts A into feature2, and all other classes from com.google into base.
*
* <p>Note that this format does not allow specifying inter-module dependencies, this is simply a
* placement tool.
*/
public class FeatureClassMapping {
HashMap<String, String> parsedRules = new HashMap<>(); // Already parsed rules.
HashSet<FeaturePredicate> mappings = new HashSet<>();
Path mappingFile;
String baseName;
static final String DEFAULT_BASE_NAME = "base";
static final String COMMENT = "#";
static final String SEPARATOR = ":";
public static FeatureClassMapping fromSpecification(Path file)
throws FeatureMappingException, IOException {
FeatureClassMapping mapping = new FeatureClassMapping();
mapping.baseName = DEFAULT_BASE_NAME;
List<String> lines = FileUtils.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
mapping.parseAndAdd(line, i);
}
return mapping;
}
public static FeatureClassMapping fromJarFiles(List<FeatureJar> featureJars, String baseName)
throws FeatureMappingException, IOException {
FeatureClassMapping mapping = new FeatureClassMapping();
mapping.baseName = baseName != null ? baseName : DEFAULT_BASE_NAME;
for (FeatureJar featureJar : featureJars) {
Path jarPath = Paths.get(featureJar.getJar());
ArchiveClassFileProvider provider = new ArchiveClassFileProvider(jarPath);
for (String javaDescriptor : provider.getClassDescriptors()) {
String javaType = DescriptorUtils.descriptorToJavaType(javaDescriptor);
mapping.addMapping(javaType, featureJar.getOutputName());
}
}
return mapping;
}
private FeatureClassMapping() {}
private void addMapping(String clazz, String feature) throws FeatureMappingException {
addRule(clazz, feature, 0);
}
FeatureClassMapping(List<String> lines) throws FeatureMappingException {
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
parseAndAdd(line, i);
}
}
public String featureForClass(String clazz) throws FeatureMappingException {
// Todo(ricow): improve performance (e.g., direct lookup of class predicates through hashmap).
FeaturePredicate bestMatch = null;
for (FeaturePredicate mapping : mappings) {
if (mapping.match(clazz)) {
if (bestMatch == null || bestMatch.predicate.length() < mapping.predicate.length()) {
bestMatch = mapping;
}
}
}
if (bestMatch == null) {
return baseName;
}
return bestMatch.feature;
}
private void parseAndAdd(String line, int lineNumber) throws FeatureMappingException {
if (line.startsWith(COMMENT)) {
return; // Ignore comments
}
if (line.isEmpty()) {
return; // Ignore blank lines
}
if (!line.contains(SEPARATOR)) {
error("Mapping lines must contain a " + SEPARATOR, lineNumber);
}
String[] values = line.split(SEPARATOR);
if (values.length != 2) {
error("Mapping lines can only contain one " + SEPARATOR, lineNumber);
}
String predicate = values[0];
String feature = values[1];
addRule(predicate, feature, lineNumber);
}
private void addRule(String predicate, String feature, int lineNumber)
throws FeatureMappingException {
if (parsedRules.containsKey(predicate)) {
if (!parsedRules.get(predicate).equals(feature)) {
error("Redefinition of predicate " + predicate + "not allowed", lineNumber);
}
return; // Already have this rule.
}
parsedRules.put(predicate, feature);
FeaturePredicate featurePredicate = new FeaturePredicate(predicate, feature);
mappings.add(featurePredicate);
}
private void error(String error, int line) throws FeatureMappingException {
throw new FeatureMappingException(
"Invalid mappings specification: " + error + "\n in file " + mappingFile + ":" + line);
}
public static class FeatureMappingException extends Exception {
FeatureMappingException(String message) {
super(message);
}
}
/** A feature predicate can either be a wildcard or class predicate. */
private static class FeaturePredicate {
private static Pattern identifier = Pattern.compile("[A-Za-z_\\-][A-Za-z0-9_$\\-]*");
final String predicate;
final String feature;
final boolean isCatchAll;
// False implies class predicate.
final boolean isWildcard;
FeaturePredicate(String predicate, String feature) throws FeatureMappingException {
isWildcard = predicate.endsWith(".*");
isCatchAll = predicate.equals("*");
if (isCatchAll) {
this.predicate = "";
} else if (isWildcard) {
this.predicate = predicate.substring(0, predicate.length() - 2);
} else {
this.predicate = predicate;
}
if (!DescriptorUtils.isValidJavaType(this.predicate) && !isCatchAll) {
throw new FeatureMappingException(this.predicate + " is not a valid identifier");
}
this.feature = feature;
}
boolean match(String className) {
if (isCatchAll) {
return true;
} else if (isWildcard) {
return className.startsWith(predicate);
} else {
// We also put inner classes into the same feature.
return className.startsWith(predicate);
}
}
}
}