com.facebook.buck.android.APKModuleGraph.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.android.APKModuleGraph.java

Source

/*
 * 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.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.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<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();
    });

    /**
     * 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;
    }

    /**
     * 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();
    }

    /**
     * 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;
                            }

                            builder.put(dexStore, translatorFunction.apply(entry.getRelativePath()));
                        }
                    });
                }
            }
        }
        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 (seedTargets.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.getDeps()) {
                        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 (BuildTarget seedTarget : seedTargets.get()) {
            final String seedModuleName = generateNameFromTarget(seedTarget);
            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.getDeps()) {
                        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) {
        return seedTargets.isPresent() && seedTargets.get().contains(depTarget);
    }

    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;
        }
    }
}