/* * Copyright 2015-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.event.ConsoleEvent; import com.facebook.buck.graph.Dot; import com.facebook.buck.json.BuildFileParseException; import com.facebook.buck.log.Logger; import com.facebook.buck.parser.PerBuildState; import com.facebook.buck.parser.SpeculativeParsing; import com.facebook.buck.query.QueryBuildTarget; import com.facebook.buck.query.QueryException; import com.facebook.buck.query.QueryExpression; import com.facebook.buck.query.QueryTarget; import com.facebook.buck.rules.Description; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.PatternsMatcher; import com.facebook.infer.annotation.SuppressFieldNotInitialized; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.TreeMultimap; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; public class QueryCommand extends AbstractCommand { private static final Logger LOG = Logger.get(QueryCommand.class); /** * Example usage: * * <pre> * buck query "allpaths('//path/to:target', '//path/to:other')" --dot > /tmp/graph.dot * dot -Tpng /tmp/graph.dot -o /tmp/graph.png * </pre> */ @Option(name = "--dot", usage = "Print result as Dot graph") private boolean generateDotOutput; @Option(name = "--bfs", usage = "Sort the dot output in bfs order") private boolean generateBFSOutput; @Option(name = "--json", usage = "Output in JSON format") private boolean generateJsonOutput; @Option( name = "--output-attributes", usage = "List of attributes to output, --output-attributes attr1 att2 ... attrN. " + "Attributes can be regular expressions. ", handler = StringSetOptionHandler.class ) @SuppressFieldNotInitialized @VisibleForTesting Supplier<ImmutableSet<String>> outputAttributes; public boolean shouldGenerateJsonOutput() { return generateJsonOutput; } public boolean shouldGenerateDotOutput() { return generateDotOutput; } public boolean shouldGenerateBFSOutput() { return generateBFSOutput; } public boolean shouldOutputAttributes() { return !outputAttributes.get().isEmpty(); } @Argument private List<String> arguments = new ArrayList<>(); @VisibleForTesting void setArguments(List<String> arguments) { this.arguments = arguments; } @Override public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException { if (arguments.isEmpty()) { params .getBuckEventBus() .post(ConsoleEvent.severe("Must specify at least the query expression")); return 1; } try (CommandThreadManager pool = new CommandThreadManager("Query", getConcurrencyLimit(params.getBuckConfig())); PerBuildState parserState = new PerBuildState( params.getParser(), params.getBuckEventBus(), pool.getExecutor(), params.getCell(), getEnableParserProfiling(), SpeculativeParsing.of(true))) { BuckQueryEnvironment env = BuckQueryEnvironment.from(params, parserState, getEnableParserProfiling()); ListeningExecutorService executor = pool.getExecutor(); return formatAndRunQuery(params, env, executor); } catch (QueryException | BuildFileParseException e) { throw new HumanReadableException(e); } } @VisibleForTesting int formatAndRunQuery( CommandRunnerParams params, BuckQueryEnvironment env, ListeningExecutorService executor) throws IOException, InterruptedException, QueryException { String queryFormat = arguments.get(0); List<String> formatArgs = arguments.subList(1, arguments.size()); if (queryFormat.contains("%Ss")) { return runSingleQueryWithSet(params, env, executor, queryFormat, formatArgs); } else if (queryFormat.contains("%s")) { return runMultipleQuery( params, env, executor, queryFormat, formatArgs, shouldGenerateJsonOutput()); } else if (formatArgs.size() > 0) { throw new HumanReadableException( "Must not specify format arguments without a %s or %Ss in the query"); } else { return runSingleQuery(params, env, executor, queryFormat); } } /** Format and evaluate the query using list substitution */ int runSingleQueryWithSet( CommandRunnerParams params, BuckQueryEnvironment env, ListeningExecutorService executor, String queryFormat, List<String> formatArgs) throws InterruptedException, QueryException, IOException { String argsList = Joiner.on(' ').join(Iterables.transform(formatArgs, input -> "'" + input + "'")); String setRepresentation = "set(" + argsList + ")"; String formattedQuery = queryFormat.replace("%Ss", setRepresentation); return runSingleQuery(params, env, executor, formattedQuery); } /** * Evaluate multiple queries in a single `buck query` run. Usage: buck query <query format> * <input1> <input2> <...> <inputN> */ static int runMultipleQuery( CommandRunnerParams params, BuckQueryEnvironment env, ListeningExecutorService executor, String queryFormat, List<String> inputsFormattedAsBuildTargets, boolean generateJsonOutput) throws IOException, InterruptedException, QueryException { if (inputsFormattedAsBuildTargets.isEmpty()) { params .getBuckEventBus() .post( ConsoleEvent.severe( "Specify one or more input targets after the query expression format")); return 1; } // Do an initial pass over the query arguments and parse them into their expressions so we can // preload all the target patterns from every argument in one go, as doing them one-by-one is // really inefficient. Set<String> targetLiterals = new LinkedHashSet<>(); for (String input : inputsFormattedAsBuildTargets) { String query = queryFormat.replace("%s", input); QueryExpression expr = QueryExpression.parse(query, env); expr.collectTargetPatterns(targetLiterals); } env.preloadTargetPatterns(targetLiterals, executor); // Now execute the query on the arguments one-by-one. TreeMultimap<String, QueryTarget> queryResultMap = TreeMultimap.create(); for (String input : inputsFormattedAsBuildTargets) { String query = queryFormat.replace("%s", input); ImmutableSet<QueryTarget> queryResult = env.evaluateQuery(query, executor); queryResultMap.putAll(input, queryResult); } LOG.debug("Printing out the following targets: " + queryResultMap); if (generateJsonOutput) { CommandHelper.printJSON(params, queryResultMap); } else { CommandHelper.printToConsole(params, queryResultMap); } return 0; } int runSingleQuery( CommandRunnerParams params, BuckQueryEnvironment env, ListeningExecutorService executor, String query) throws IOException, InterruptedException, QueryException { ImmutableSet<QueryTarget> queryResult = env.evaluateQuery(query, executor); LOG.debug("Printing out the following targets: " + queryResult); if (shouldOutputAttributes()) { collectAndPrintAttributes(params, env, queryResult); } else if (shouldGenerateDotOutput()) { printDotOutput(params, env, queryResult); } else if (shouldGenerateJsonOutput()) { CommandHelper.printJSON(params, queryResult); } else { CommandHelper.printToConsole(params, queryResult); } return 0; } private void printDotOutput( CommandRunnerParams params, BuckQueryEnvironment env, Set<QueryTarget> queryResult) throws IOException, QueryException { Dot.writeSubgraphOutput( env.getTargetGraph(), "result_graph", env.getNodesFromQueryTargets(queryResult), targetNode -> "\"" + targetNode.getBuildTarget().getFullyQualifiedName() + "\"", targetNode -> Description.getBuildRuleType(targetNode.getDescription()).getName(), params.getConsole().getStdOut(), shouldGenerateBFSOutput()); } private void collectAndPrintAttributes( CommandRunnerParams params, BuckQueryEnvironment env, Set<QueryTarget> queryResult) throws QueryException { PatternsMatcher patternsMatcher = new PatternsMatcher(outputAttributes.get()); SortedMap<String, SortedMap<String, Object>> result = new TreeMap<>(); for (QueryTarget target : queryResult) { if (!(target instanceof QueryBuildTarget)) { continue; } TargetNode<?, ?> node = env.getNode(target); try { SortedMap<String, Object> sortedTargetRule = params.getParser().getRawTargetNode(env.getParserState(), params.getCell(), node); if (sortedTargetRule == null) { params .getConsole() .printErrorText( "unable to find rule for target " + node.getBuildTarget().getFullyQualifiedName()); continue; } SortedMap<String, Object> attributes = new TreeMap<>(); if (patternsMatcher.hasPatterns()) { for (String key : sortedTargetRule.keySet()) { String snakeCaseKey = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, key); if (patternsMatcher.matches(snakeCaseKey)) { attributes.put(snakeCaseKey, sortedTargetRule.get(key)); } } } result.put( node.getBuildTarget().getUnflavoredBuildTarget().getFullyQualifiedName(), attributes); } catch (BuildFileParseException e) { params .getConsole() .printErrorText( "unable to find rule for target " + node.getBuildTarget().getFullyQualifiedName()); continue; } } StringWriter stringWriter = new StringWriter(); try { ObjectMappers.WRITER.withDefaultPrettyPrinter().writeValue(stringWriter, result); } catch (IOException e) { // Shouldn't be possible while writing to a StringWriter... throw new RuntimeException(e); } String output = stringWriter.getBuffer().toString(); params.getConsole().getStdOut().println(output); } @Override public boolean isReadOnly() { return true; } @Override public String getShortDescription() { return "provides facilities to query information about the target nodes graph"; } public static String getEscapedArgumentsListAsString(List<String> arguments) { return Joiner.on(" ").join(Lists.transform(arguments, arg -> "'" + arg + "'")); } static String getAuditDependenciesQueryFormat(boolean isTransitive, boolean includeTests) { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.append(isTransitive ? "deps('%s') " : "deps('%s', 1) "); if (includeTests) { queryBuilder.append(isTransitive ? "union deps(testsof(deps('%s')))" : "union testsof('%s')"); } queryBuilder.append(" except set('%s')"); return queryBuilder.toString(); } /** @return the equivalent 'buck query' call to 'buck audit dependencies'. */ static String buildAuditDependenciesQueryExpression( List<String> arguments, boolean isTransitive, boolean includeTests, boolean jsonOutput) { StringBuilder queryBuilder = new StringBuilder("buck query "); queryBuilder.append("\"" + getAuditDependenciesQueryFormat(isTransitive, includeTests) + "\" "); queryBuilder.append(getEscapedArgumentsListAsString(arguments)); if (jsonOutput) { queryBuilder.append(" --json"); } return queryBuilder.toString(); } /** @return the equivalent 'buck query' call to 'buck audit tests'. */ static String buildAuditTestsQueryExpression(List<String> arguments, boolean jsonOutput) { StringBuilder queryBuilder = new StringBuilder("buck query \"testsof('%s')\" "); queryBuilder.append(getEscapedArgumentsListAsString(arguments)); if (jsonOutput) { queryBuilder.append(" --json"); } return queryBuilder.toString(); } /** @return the equivalent 'buck query' call to 'buck audit owner'. */ static String buildAuditOwnerQueryExpression(List<String> arguments, boolean jsonOutput) { StringBuilder queryBuilder = new StringBuilder("buck query \"owner('%s')\" "); queryBuilder.append(getEscapedArgumentsListAsString(arguments)); if (jsonOutput) { queryBuilder.append(" --json"); } return queryBuilder.toString(); } }