/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.android;
import com.facebook.buck.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.graph.DirectedAcyclicGraph;
import com.facebook.buck.graph.MutableDirectedGraph;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.classes.ClasspathTraversal;
import com.facebook.buck.jvm.java.classes.ClasspathTraverser;
import com.facebook.buck.jvm.java.classes.DefaultClasspathTraverser;
import com.facebook.buck.jvm.java.classes.FileLike;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Utility class for grouping sets of targets and their dependencies into APK Modules containing
* their exclusive dependencies. Targets that are dependencies of the root target are included in
* the root. Targets that are dependencies of two or more groups but not dependencies of the root
* are added to their own group.
*/
public class APKModuleGraph {
static final String ROOT_APKMODULE_NAME = "dex";
private final TargetGraph targetGraph;
private final BuildTarget target;
private final Optional<Map<String, List<BuildTarget>>> suppliedSeedConfigMap;
private final Optional<Set<BuildTarget>> seedTargets;
private final Supplier<ImmutableMap<BuildTarget, APKModule>> targetToModuleMapSupplier =
Suppliers.memoize(
new Supplier<ImmutableMap<BuildTarget, APKModule>>() {
@Override
public ImmutableMap<BuildTarget, APKModule> get() {
final ImmutableMap.Builder<BuildTarget, APKModule> mapBuilder =
ImmutableMap.builder();
new AbstractBreadthFirstTraversal<APKModule>(
getGraph().getNodesWithNoIncomingEdges()) {
@Override
public ImmutableSet<APKModule> visit(final APKModule node) {
if (node.equals(rootAPKModuleSupplier.get())) {
return ImmutableSet.of();
}
node.getBuildTargets().forEach(input -> mapBuilder.put(input, node));
return getGraph().getOutgoingNodesFor(node);
}
}.start();
return mapBuilder.build();
}
});
private final Supplier<APKModule> rootAPKModuleSupplier =
Suppliers.memoize(this::generateRootModule);
private final Supplier<DirectedAcyclicGraph<APKModule>> graphSupplier =
Suppliers.memoize(this::generateGraph);
private final Supplier<ImmutableSet<APKModule>> modulesSupplier =
Suppliers.memoize(
() -> {
final ImmutableSet.Builder<APKModule> moduleBuilder = ImmutableSet.builder();
new AbstractBreadthFirstTraversal<APKModule>(getRootAPKModule()) {
@Override
public Iterable<APKModule> visit(APKModule apkModule) throws RuntimeException {
moduleBuilder.add(apkModule);
return getGraph().getIncomingNodesFor(apkModule);
}
}.start();
return moduleBuilder.build();
});
private final Supplier<Optional<Map<String, List<BuildTarget>>>> configMapSupplier =
Suppliers.memoize(this::generateSeedConfigMap);
/**
* Constructor for the {@code APKModule} graph generator object
*
* @param seedConfigMap A map of names to seed targets to use for creating {@code APKModule}.
* @param targetGraph The full target graph of the build
* @param target The root target to use to traverse the graph
*/
public APKModuleGraph(
final Optional<Map<String, List<BuildTarget>>> seedConfigMap,
final TargetGraph targetGraph,
final BuildTarget target) {
this.targetGraph = targetGraph;
this.target = target;
this.seedTargets = Optional.empty();
this.suppliedSeedConfigMap = seedConfigMap;
}
/**
* Constructor for the {@code APKModule} graph generator object
*
* @param targetGraph The full target graph of the build
* @param target The root target to use to traverse the graph
* @param seedTargets The set of seed targets to use for creating {@code APKModule}.
*/
public APKModuleGraph(
final TargetGraph targetGraph,
final BuildTarget target,
final Optional<Set<BuildTarget>> seedTargets) {
this.targetGraph = targetGraph;
this.target = target;
this.seedTargets = seedTargets;
this.suppliedSeedConfigMap = Optional.empty();
}
private Optional<Map<String, List<BuildTarget>>> generateSeedConfigMap() {
if (suppliedSeedConfigMap.isPresent()) {
return suppliedSeedConfigMap;
}
if (!seedTargets.isPresent()) {
return Optional.empty();
}
HashMap<String, List<BuildTarget>> seedConfigMapMutable = new HashMap<>();
for (BuildTarget seedTarget : seedTargets.get()) {
final String moduleName = generateNameFromTarget(seedTarget);
seedConfigMapMutable.put(moduleName, ImmutableList.of(seedTarget));
}
ImmutableMap<String, List<BuildTarget>> seedConfigMapImmutable =
ImmutableMap.copyOf(seedConfigMapMutable);
return Optional.of(seedConfigMapImmutable);
}
/**
* Lazy generate the graph on first use
*
* @return the DAG representing APKModules and their dependency relationships
*/
public DirectedAcyclicGraph<APKModule> getGraph() {
return graphSupplier.get();
}
/**
* Get the APKModule representing the core application that is always included in the apk
*
* @return the root APK Module
*/
public APKModule getRootAPKModule() {
return rootAPKModuleSupplier.get();
}
public ImmutableSet<APKModule> getAPKModules() {
return modulesSupplier.get();
}
public Optional<Map<String, List<BuildTarget>>> getSeedConfigMap() {
return configMapSupplier.get();
}
/**
* Get the Module that contains the given target
*
* @param target target to serach for in modules
* @return the module that contains the target
*/
public APKModule findModuleForTarget(BuildTarget target) {
APKModule module = targetToModuleMapSupplier.get().get(target);
return (module == null ? rootAPKModuleSupplier.get() : module);
}
/**
* Group the classes in the input jars into a multimap based on the APKModule they belong to
*
* @param apkModuleToJarPathMap the mapping of APKModules to the path for the jar files
* @param translatorFunction function used to translate obfuscated names
* @param filesystem filesystem representation for resolving paths
* @return The mapping of APKModules to the class names they contain
* @throws IOException
*/
public static ImmutableMultimap<APKModule, String> getAPKModuleToClassesMap(
final ImmutableMultimap<APKModule, Path> apkModuleToJarPathMap,
final Function<String, String> translatorFunction,
final ProjectFilesystem filesystem)
throws IOException {
final ImmutableMultimap.Builder<APKModule, String> builder = ImmutableMultimap.builder();
if (!apkModuleToJarPathMap.isEmpty()) {
for (final APKModule dexStore : apkModuleToJarPathMap.keySet()) {
for (Path jarFilePath : apkModuleToJarPathMap.get(dexStore)) {
ClasspathTraverser classpathTraverser = new DefaultClasspathTraverser();
classpathTraverser.traverse(
new ClasspathTraversal(ImmutableSet.of(jarFilePath), filesystem) {
@Override
public void visit(FileLike entry) {
if (!entry.getRelativePath().endsWith(".class")) {
// ignore everything but class files in the jar.
return;
}
String classpath = entry.getRelativePath().replaceAll("\\.class$", "");
builder.put(dexStore, translatorFunction.apply(classpath));
}
});
}
}
}
return builder.build();
}
/**
* Generate the graph by identifying root targets, then marking targets with the seeds they are
* reachable with, then consolidating the targets reachable by multiple seeds into shared modules
*
* @return The graph of APKModules with edges representing dependencies between modules
*/
private DirectedAcyclicGraph<APKModule> generateGraph() {
final MutableDirectedGraph<APKModule> apkModuleGraph = new MutableDirectedGraph<>();
apkModuleGraph.addNode(rootAPKModuleSupplier.get());
if (getSeedConfigMap().isPresent()) {
HashMultimap<BuildTarget, String> targetToContainingApkModulesMap =
mapTargetsToContainingModules();
generateSharedModules(apkModuleGraph, targetToContainingApkModulesMap);
}
return new DirectedAcyclicGraph<>(apkModuleGraph);
}
/**
* This walks through the target graph starting from the root target and adds all reachable
* targets that are not seed targets to the root module
*
* @return The root APK Module
*/
private APKModule generateRootModule() {
final Set<BuildTarget> rootTargets = new HashSet<>();
if (targetGraph != TargetGraph.EMPTY) {
new AbstractBreadthFirstTraversal<TargetNode<?, ?>>(targetGraph.get(target)) {
@Override
public ImmutableSet<TargetNode<?, ?>> visit(TargetNode<?, ?> node) {
ImmutableSet.Builder<TargetNode<?, ?>> depsBuilder = ImmutableSet.builder();
for (BuildTarget depTarget : node.getBuildDeps()) {
if (!isSeedTarget(depTarget)) {
depsBuilder.add(targetGraph.get(depTarget));
rootTargets.add(depTarget);
}
}
return depsBuilder.build();
}
}.start();
}
return APKModule.builder().setName(ROOT_APKMODULE_NAME).setBuildTargets(rootTargets).build();
}
/**
* For each seed target, find its reachable targets and mark them in a multimap as being reachable
* by that module for later sorting into exclusive and shared targets
*
* @return the Multimap containing targets and the seed modules that contain them
*/
private HashMultimap<BuildTarget, String> mapTargetsToContainingModules() {
final HashMultimap<BuildTarget, String> targetToContainingApkModuleNameMap =
HashMultimap.create();
for (Map.Entry<String, List<BuildTarget>> seedConfig : getSeedConfigMap().get().entrySet()) {
final String seedModuleName = seedConfig.getKey();
for (BuildTarget seedTarget : seedConfig.getValue()) {
targetToContainingApkModuleNameMap.put(seedTarget, seedModuleName);
new AbstractBreadthFirstTraversal<TargetNode<?, ?>>(targetGraph.get(seedTarget)) {
@Override
public ImmutableSet<TargetNode<?, ?>> visit(TargetNode<?, ?> node) {
ImmutableSet.Builder<TargetNode<?, ?>> depsBuilder = ImmutableSet.builder();
for (BuildTarget depTarget : node.getBuildDeps()) {
if (!isInRootModule(depTarget) && !isSeedTarget(depTarget)) {
depsBuilder.add(targetGraph.get(depTarget));
targetToContainingApkModuleNameMap.put(depTarget, seedModuleName);
}
}
return depsBuilder.build();
}
}.start();
}
}
return targetToContainingApkModuleNameMap;
}
/**
* Loop through each of the targets we visited while generating seed modules: If the are exclusive
* to that module, add them to that module. If they are not exclusive to that module, find or
* create an appropriate shared module and fill out its dependencies
*
* @param apkModuleGraph the current graph we're building
* @param targetToContainingApkModulesMap the targets mapped to the seed targets they are
* reachable from
*/
private void generateSharedModules(
MutableDirectedGraph<APKModule> apkModuleGraph,
HashMultimap<BuildTarget, String> targetToContainingApkModulesMap) {
// Sort the targets into APKModuleBuilders based on their seed dependencies
final Map<ImmutableSet<String>, APKModule.Builder> combinedModuleHashToModuleMap =
new HashMap<>();
for (Map.Entry<BuildTarget, Collection<String>> entry :
targetToContainingApkModulesMap.asMap().entrySet()) {
ImmutableSet<String> containingModuleSet = ImmutableSet.copyOf(entry.getValue());
boolean exists = false;
for (Map.Entry<ImmutableSet<String>, APKModule.Builder> existingEntry :
combinedModuleHashToModuleMap.entrySet()) {
if (existingEntry.getKey().equals(containingModuleSet)) {
existingEntry.getValue().addBuildTargets(entry.getKey());
exists = true;
break;
}
}
if (!exists) {
String name =
containingModuleSet.size() == 1
? containingModuleSet.iterator().next()
: generateNameFromTarget(entry.getKey());
combinedModuleHashToModuleMap.put(
containingModuleSet, APKModule.builder().setName(name).addBuildTargets(entry.getKey()));
}
}
// Find the seed modules and add them to the graph
Map<String, APKModule> seedModules = new HashMap<>();
for (Map.Entry<ImmutableSet<String>, APKModule.Builder> entry :
combinedModuleHashToModuleMap.entrySet()) {
if (entry.getKey().size() == 1) {
APKModule seed = entry.getValue().build();
apkModuleGraph.addNode(seed);
seedModules.put(entry.getKey().iterator().next(), seed);
apkModuleGraph.addEdge(seed, rootAPKModuleSupplier.get());
}
}
// Find the shared modules and add them to the graph
for (Map.Entry<ImmutableSet<String>, APKModule.Builder> entry :
combinedModuleHashToModuleMap.entrySet()) {
if (entry.getKey().size() > 1) {
APKModule shared = entry.getValue().build();
apkModuleGraph.addNode(shared);
apkModuleGraph.addEdge(shared, rootAPKModuleSupplier.get());
for (String seedName : entry.getKey()) {
apkModuleGraph.addEdge(seedModules.get(seedName), shared);
}
}
}
}
private boolean isInRootModule(BuildTarget depTarget) {
ImmutableSet<BuildTarget> rootTargets = rootAPKModuleSupplier.get().getBuildTargets();
return rootTargets != null && rootTargets.contains(depTarget);
}
private boolean isSeedTarget(BuildTarget depTarget) {
if (!getSeedConfigMap().isPresent()) {
return false;
}
for (List<BuildTarget> targetsPerConfig : getSeedConfigMap().get().values()) {
if (targetsPerConfig.contains(depTarget)) {
return true;
}
}
return false;
}
private static String generateNameFromTarget(BuildTarget androidModuleTarget) {
String replacementPattern = "[/\\\\#-]";
String shortName =
androidModuleTarget.getShortNameAndFlavorPostfix().replaceAll(replacementPattern, ".");
String name = androidModuleTarget.getBasePath().toString().replaceAll(replacementPattern, ".");
if (name.endsWith(shortName)) {
// return just the base path, ignoring the target name that is the same as its parent
return name;
} else {
return name.isEmpty() ? shortName : name + "." + shortName;
}
}
}