// Copyright 2014 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.lib.runtime.commands; import static com.google.devtools.build.lib.packages.Rule.ALL_LABELS; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.analysis.NoBuildEvent; import com.google.devtools.build.lib.analysis.NoBuildRequestFinishedEvent; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.pkgcache.PackageCacheOptions; import com.google.devtools.build.lib.query2.AbstractBlazeQueryEnvironment; import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting; import com.google.devtools.build.lib.query2.engine.QueryEvalResult; import com.google.devtools.build.lib.query2.engine.QueryException; import com.google.devtools.build.lib.query2.engine.QueryExpression; import com.google.devtools.build.lib.query2.engine.QueryUtil; import com.google.devtools.build.lib.query2.engine.QueryUtil.AggregateAllOutputFormatterCallback; import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback; import com.google.devtools.build.lib.query2.output.OutputFormatter; import com.google.devtools.build.lib.query2.output.OutputFormatter.StreamedFormatter; import com.google.devtools.build.lib.query2.output.QueryOptions; import com.google.devtools.build.lib.query2.output.QueryOutputUtils; import com.google.devtools.build.lib.runtime.BlazeCommand; import com.google.devtools.build.lib.runtime.BlazeCommandResult; import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.runtime.KeepGoingOption; import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption; import com.google.devtools.build.lib.util.AbruptExitException; import com.google.devtools.build.lib.util.ExitCode; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsProvider; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.ClosedByInterruptException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; /** Command line wrapper for executing a query with blaze. */ @Command( name = "query", options = { PackageCacheOptions.class, QueryOptions.class, KeepGoingOption.class, LoadingPhaseThreadsOption.class }, help = "resource:query.txt", shortDescription = "Executes a dependency graph query.", allowResidue = true, binaryStdOut = true, completion = "label", canRunInOutputDirectory = true ) public final class QueryCommand implements BlazeCommand { @Override public void editOptions(OptionsParser optionsParser) { } /** * Exit codes: * 0 on successful evaluation. * 1 if query evaluation did not complete. * 2 if query parsing failed. * 3 if errors were reported but evaluation produced a partial result * (only when --keep_going is in effect.) */ @Override public BlazeCommandResult exec(CommandEnvironment env, OptionsProvider options) { BlazeRuntime runtime = env.getRuntime(); QueryOptions queryOptions = options.getOptions(QueryOptions.class); try { env.setupPackageCache(options, runtime.getDefaultsPackageContent()); } catch (InterruptedException e) { env.getReporter().handle(Event.error("query interrupted")); return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED); } catch (AbruptExitException e) { env.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage())); return BlazeCommandResult.exitCode(e.getExitCode()); } String query; if (!options.getResidue().isEmpty()) { if (!queryOptions.queryFile.isEmpty()) { env.getReporter() .handle(Event.error("Command-line query and --query_file cannot both be specified")); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } query = Joiner.on(' ').join(options.getResidue()); } else if (!queryOptions.queryFile.isEmpty()) { // Works for absolute or relative query file. Path residuePath = env.getWorkingDirectory().getRelative(queryOptions.queryFile); try { query = new String(FileSystemUtils.readContent(residuePath), StandardCharsets.UTF_8); } catch (IOException e) { env.getReporter() .handle(Event.error("I/O error reading from " + residuePath.getPathString())); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } } else { env.getReporter().handle(Event.error(String.format( "missing query expression. Type '%s help query' for syntax and help", runtime.getProductName()))); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } Iterable formatters = runtime.getQueryOutputFormatters(); OutputFormatter formatter = OutputFormatter.getFormatter(formatters, queryOptions.outputFormat); if (formatter == null) { env.getReporter().handle(Event.error( String.format("Invalid output format '%s'. Valid values are: %s", queryOptions.outputFormat, OutputFormatter.formatterNames(formatters)))); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } Set settings = queryOptions.toSettings(); boolean streamResults = QueryOutputUtils.shouldStreamResults(queryOptions, formatter); QueryEvalResult result; AbstractBlazeQueryEnvironment queryEnv = newQueryEnvironment( env, options.getOptions(KeepGoingOption.class).keepGoing, !streamResults, queryOptions.universeScope, options.getOptions(LoadingPhaseThreadsOption.class).threads, settings); QueryExpression expr; try { expr = QueryExpression.parse(query, queryEnv); } catch (QueryException e) { env.getReporter() .handle(Event.error(null, "Error while parsing '" + query + "': " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } try { formatter.verifyCompatible(queryEnv, expr); } catch (QueryException e) { env.getReporter().handle(Event.error(e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } expr = queryEnv.transformParsedQuery(expr); OutputStream out; if (formatter.canBeBuffered()) { // There is no particular reason for the 16384 constant here, except its a multiple of the // gRPC buffer size. We mainly don't want to send each label individually because the output // stream is connected to gRPC, and every write gets converted to one gRPC call. out = new BufferedOutputStream(env.getReporter().getOutErr().getOutputStream(), 16384); } else { out = env.getReporter().getOutErr().getOutputStream(); } ThreadSafeOutputFormatterCallback callback; if (streamResults) { disableAnsiCharactersFiltering(env); StreamedFormatter streamedFormatter = ((StreamedFormatter) formatter); streamedFormatter.setOptions( queryOptions, queryOptions.aspectDeps.createResolver(env.getPackageManager(), env.getReporter())); callback = streamedFormatter.createStreamCallback(out, queryOptions, queryEnv); } else { callback = QueryUtil.newOrderedAggregateAllOutputFormatterCallback(queryEnv); } boolean catastrophe = true; try { result = queryEnv.evaluateQuery(expr, callback); catastrophe = false; } catch (QueryException e) { catastrophe = false; // Keep consistent with reportBuildFileError() env.getReporter() // TODO(bazel-team): this is a kludge to fix a bug observed in the wild. We should make // sure no null error messages ever get in. .handle(Event.error(e.getMessage() == null ? e.toString() : e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.ANALYSIS_FAILURE); } catch (InterruptedException e) { catastrophe = false; IOException ioException = callback.getIoException(); if (ioException == null || ioException instanceof ClosedByInterruptException) { env.getReporter().handle(Event.error("query interrupted")); return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED); } else { env.getReporter().handle(Event.error("I/O error: " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } } catch (IOException e) { catastrophe = false; env.getReporter().handle(Event.error("I/O error: " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } finally { if (!catastrophe) { try { out.flush(); } catch (IOException e) { env.getReporter().handle( Event.error("Failed to flush query results: " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } } } env.getEventBus().post(new NoBuildEvent(env.getCommandName(), env.getCommandStartTime(), true)); if (!streamResults) { disableAnsiCharactersFiltering(env); try { Set targets = ((AggregateAllOutputFormatterCallback) callback).getResult(); QueryOutputUtils.output( queryOptions, result, targets, formatter, out, queryOptions.aspectDeps.createResolver(env.getPackageManager(), env.getReporter())); } catch (ClosedByInterruptException | InterruptedException e) { env.getReporter().handle(Event.error("query interrupted")); return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED); } catch (IOException e) { env.getReporter().handle(Event.error("I/O error: " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } finally { try { out.flush(); } catch (IOException e) { env.getReporter().handle( Event.error("Failed to flush query results: " + e.getMessage())); return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } } } if (result.isEmpty()) { env.getReporter().handle(Event.info("Empty results")); } ExitCode exitCode = result.getSuccess() ? ExitCode.SUCCESS : ExitCode.PARTIAL_ANALYSIS_FAILURE; env.getEventBus() .post(new NoBuildRequestFinishedEvent(exitCode, runtime.getClock().currentTimeMillis())); return BlazeCommandResult.exitCode(exitCode); } /** * When Blaze is used with --color=no or not in a tty a ansi characters filter is set so that * we don't print fancy colors in non-supporting terminal outputs. But query output, specifically * the binary formatters, can print actual data that contain ansi bytes/chars. Because of that * we need to remove the filtering before printing any query result. */ private static void disableAnsiCharactersFiltering(CommandEnvironment env) { env.getReporter().switchToAnsiAllowingHandler(); } @VisibleForTesting // for com.google.devtools.deps.gquery.test.QueryResultTestUtil public static AbstractBlazeQueryEnvironment newQueryEnvironment(CommandEnvironment env, boolean keepGoing, boolean orderedResults, int loadingPhaseThreads, Set settings) { return newQueryEnvironment(env, keepGoing, orderedResults, ImmutableList.of(), loadingPhaseThreads, settings); } public static AbstractBlazeQueryEnvironment newQueryEnvironment(CommandEnvironment env, boolean keepGoing, boolean orderedResults, List universeScope, int loadingPhaseThreads, Set settings) { return env.getRuntime() .getQueryEnvironmentFactory() .create( env.getPackageManager().newTransitiveLoader(), env.getSkyframeExecutor(), env.getPackageManager(), env.newTargetPatternEvaluator(), keepGoing, /*strictScope=*/ true, orderedResults, universeScope, loadingPhaseThreads, /*labelFilter=*/ ALL_LABELS, env.getReporter(), settings, env.getRuntime().getQueryFunctions(), env.getPackageManager().getPackagePath(), /*blockUniverseEvaluationErrors=*/ false); } }