| /* |
| * Copyright 2013-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.cli; |
| |
| import com.facebook.buck.json.BuildFileParseException; |
| import com.facebook.buck.model.BuildTargetException; |
| import com.facebook.buck.parser.PartialGraph; |
| import com.facebook.buck.parser.RawRulePredicates; |
| import com.facebook.buck.rules.BuildRule; |
| import com.facebook.buck.rules.DependencyGraph; |
| import com.facebook.buck.util.BuckConstant; |
| import com.facebook.buck.util.HumanReadableException; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| |
| import org.neo4j.graphdb.Direction; |
| import org.neo4j.graphdb.GraphDatabaseService; |
| import org.neo4j.graphdb.Node; |
| import org.neo4j.graphdb.Path; |
| import org.neo4j.graphdb.RelationshipType; |
| import org.neo4j.graphdb.Transaction; |
| import org.neo4j.graphdb.factory.GraphDatabaseFactory; |
| import org.neo4j.graphdb.traversal.Evaluator; |
| import org.neo4j.graphdb.traversal.Evaluators; |
| import org.neo4j.kernel.Traversal; |
| import org.neo4j.kernel.Uniqueness; |
| |
| import java.io.IOException; |
| import java.nio.file.Paths; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.SortedMap; |
| |
| public class QueryCommand extends AbstractCommandRunner<QueryCommandOptions> { |
| /** |
| * Path (relative to project root) to the scratch directory where the neo4j graph database will be |
| * written. |
| */ |
| static final java.nio.file.Path DB_PATH = Paths.get(BuckConstant.BIN_DIR, "querydb"); |
| |
| /** |
| * Right now there is only one kind of relationship we store in our dependency |
| * graph: depends-on. |
| */ |
| static enum QueryRelType implements RelationshipType { |
| DEP, |
| } |
| |
| public QueryCommand(CommandRunnerParams params) { |
| super(params); |
| } |
| |
| @Override |
| QueryCommandOptions createOptions(BuckConfig buckConfig) { |
| return new QueryCommandOptions(buckConfig); |
| } |
| |
| @Override |
| int runCommandWithOptionsInternal(QueryCommandOptions options) |
| throws IOException, HumanReadableException { |
| if (options.getArguments().size() != 1) { |
| getStdErr().printf( |
| "buck query expects 1 argument, but received %d.\n", |
| options.getArguments().size()); |
| return 1; |
| } |
| |
| DependencyQuery query = DependencyQuery.parseQueryString(options.getArguments().get(0)); |
| |
| PartialGraph graph; |
| try { |
| graph = PartialGraph.createPartialGraph( |
| RawRulePredicates.matchName(query.getTarget()), |
| getProjectFilesystem(), |
| options.getDefaultIncludes(), |
| getParser(), |
| getBuckEventBus()); |
| } catch (BuildFileParseException | BuildTargetException e) { |
| console.printBuildFailureWithoutStacktrace(e); |
| return 1; |
| } |
| |
| // String output has no trailing newline. |
| String output = executeQuery(graph, query); |
| getStdOut().println(output); |
| return 0; |
| } |
| |
| /** |
| * Given a parsed query and a PartialGraph, executes the dependency query and returns |
| * output fit for the terminal. Internally this method creates, populates, and queries a neo4j |
| * database, but this should be transparent to the caller and the implementation could be |
| * changed in the future. |
| * |
| * @param partialGraph dependency graph including the target we are querying. |
| * @param query dependency query we want to run. |
| * @return String output to be printed with no trailing newline. |
| * @throws HumanReadableException on unrecognized build targets. |
| */ |
| @VisibleForTesting |
| String executeQuery(PartialGraph partialGraph, DependencyQuery query) |
| throws HumanReadableException { |
| // Clear the on-disk database and rebuild each time, this could be inefficient. |
| clearDbPath(); |
| |
| String pathToDatabaseDirectory = getProjectFilesystem().getFileForRelativePath(DB_PATH) |
| .getAbsolutePath(); |
| GraphDatabaseService graphDb = new GraphDatabaseFactory().newEmbeddedDatabase( |
| pathToDatabaseDirectory); |
| registerShutdownHook(graphDb); |
| |
| String output; |
| try (Transaction tx = graphDb.beginTx()) { |
| Map<String, Node> nameNodeMap = populateNeo4j(graphDb, partialGraph); |
| |
| if (!nameNodeMap.containsKey(query.getTarget())) { |
| throw new HumanReadableException( |
| String.format("Unknown build target: %s.", query.getTarget())); |
| } |
| |
| if (query.getSource().isPresent()) { |
| // Process a path query. |
| String presentSource = query.getSource().get(); |
| if (!nameNodeMap.containsKey(presentSource)) { |
| throw new HumanReadableException( |
| String.format("Unknown build target: %s.", presentSource)); |
| } |
| output = traversePaths( |
| nameNodeMap.get(query.getTarget()), |
| nameNodeMap.get(presentSource), |
| query.getDepth(), |
| /* shortestPathOnly */ true); |
| } else { |
| // Process a dependency query. |
| output = traverseDependencies( |
| nameNodeMap.get(query.getTarget()), |
| query.getDepth()); |
| } |
| |
| tx.success(); |
| } finally { |
| // Closing a transaction does not shutdown the database, so we make sure to do it here |
| // despite the try-with-resources. |
| graphDb.shutdown(); |
| } |
| |
| return output; |
| } |
| |
| /** |
| * Copies the Buck graph into the neo4j database. |
| * |
| * @param graphDb graph database that has been initialized. |
| * @param partialGraph that we want to load into neo4j. |
| * @return map from fully qualified names to nodes in neo4j. |
| */ |
| private Map<String, Node> populateNeo4j( |
| GraphDatabaseService graphDb, |
| PartialGraph partialGraph) { |
| SortedMap<String, Node> nameNodeMap = Maps.newTreeMap(); |
| |
| DependencyGraph dependencyGraph = partialGraph.getDependencyGraph(); |
| |
| // Add nodes, which are BuildRules named by their BuildTarget. |
| for (BuildRule rule : dependencyGraph.getNodes()) { |
| Node node = graphDb.createNode(); |
| String nodeName = rule.getFullyQualifiedName(); |
| node.setProperty("name", nodeName); |
| nameNodeMap.put(nodeName, node); |
| } |
| |
| // Add edges. |
| for (BuildRule rule : dependencyGraph.getNodes()) { |
| Node source = nameNodeMap.get(rule.getFullyQualifiedName()); |
| for (BuildRule dep : rule.getDeps()) { |
| Node sink = nameNodeMap.get(dep.getFullyQualifiedName()); |
| source.createRelationshipTo(sink, QueryRelType.DEP); |
| } |
| } |
| |
| return nameNodeMap; |
| } |
| |
| /** |
| * Traverse the graph tracking all of the transitive dependencies for a target node |
| * up to a certain depth. |
| * |
| * @param target node we find dependencies of. |
| * @param depth number of steps we trace backwards in a BFS. |
| * @return String output of all of the dependencies. Does not include trailing newline. |
| */ |
| private String traverseDependencies(Node target, Optional<Integer> depth) { |
| Evaluator eval = (depth.isPresent()) ? Evaluators.toDepth(depth.get()) : Evaluators.all(); |
| |
| Iterable<Node> dependencyNodes = Traversal.description() |
| .breadthFirst() |
| .relationships(QueryRelType.DEP, Direction.OUTGOING) |
| .evaluator(eval) |
| .traverse(target) |
| .nodes(); |
| |
| List<String> nodeNames = Lists.newArrayList(); |
| for (Node dep : dependencyNodes) { |
| nodeNames.add((String)dep.getProperty("name")); |
| } |
| return Joiner.on('\n').join(nodeNames); |
| } |
| |
| /** |
| * Traverse all or one of the dependency paths from a target back to one of its dependencies. |
| * |
| * @param target node which has dependencies. |
| * @param source node which is depended upon. |
| * @param depth number of steps we trace backwards in a BFS. If absent we do not limit the |
| * depth and perform a full traversal. |
| * @param shortestPathOnly dictates whether we find all paths or just the shortest. |
| * @return output of all of the dependency paths. Does not includes trailing newline. |
| */ |
| private String traversePaths(Node target, |
| Node source, Optional<Integer> depth, |
| boolean shortestPathOnly) { |
| List<String> outputLines = Lists.newArrayList(); |
| Evaluator eval = (depth.isPresent()) ? Evaluators.toDepth(depth.get()) : Evaluators.all(); |
| Uniqueness uniq = shortestPathOnly ? Uniqueness.NODE_GLOBAL : Uniqueness.NONE; |
| |
| Iterable<Path> dependencyPaths = Traversal.description() |
| .breadthFirst() |
| .relationships(QueryRelType.DEP, Direction.OUTGOING) |
| .evaluator(eval) |
| .evaluator(Evaluators.includeWhereEndNodeIs(source)) |
| .uniqueness(uniq) |
| .traverse(target); |
| for (Path path : dependencyPaths) { |
| Iterable<String> nodeNames = Iterables.transform( |
| path.nodes(), |
| new Function<Node, String>() { |
| @Override |
| public String apply(Node node) { |
| return (String) node.getProperty("name"); |
| } |
| }); |
| outputLines.add(Joiner.on(" -> ").join(nodeNames)); |
| } |
| return Joiner.on('\n').join(outputLines); |
| } |
| |
| |
| /** |
| * Registers a shutdown hook for the Neo4j instance so that it shuts down nicely |
| * when the VM exits (even if you "Ctrl-C" the running application). |
| * |
| * @param graphDb database service to shut down. |
| */ |
| private static void registerShutdownHook(final GraphDatabaseService graphDb) { |
| Runtime.getRuntime().addShutdownHook(new Thread() { |
| @Override |
| public void run() { |
| graphDb.shutdown(); |
| } |
| }); |
| } |
| |
| private void clearDbPath() { |
| try { |
| getProjectFilesystem().rmdir(DB_PATH); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| String getUsageIntro() { |
| return "performs a neo4j query on the dependency graph"; |
| } |
| } |