diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequestOptions.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequestOptions.java index e0b5388cf49724..adf1fcc1d4d4bc 100644 --- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequestOptions.java +++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequestOptions.java @@ -402,6 +402,29 @@ public String getSymlinkPrefix(String productName) { + "line. It is an error to specify a file here as well as command-line patterns.") public String targetPatternFile; + @Option( + name = "target_query", + defaultValue = "", + documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS, + effectTags = {OptionEffectTag.CHANGES_INPUTS}, + help = + "If set, build will evaluate the query expression and build the resulting targets. " + + "Example: --target_query='deps(//foo) - deps(//bar)'. May be combined with " + + "command-line target patterns. Cannot be used with --target_pattern_file or " + + "--target_query_file.") + public String query; + + @Option( + name = "target_query_file", + defaultValue = "", + documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS, + effectTags = {OptionEffectTag.CHANGES_INPUTS}, + help = + "If set, build will read a query expression from the file named here and build the " + + "resulting targets. May be combined with command-line target patterns. Cannot be " + + "used with --target_pattern_file or --target_query.") + public String queryFile; + /** * Do not use directly. Instead use {@link * com.google.devtools.build.lib.runtime.CommandEnvironment#withMergedAnalysisAndExecutionSourceOfTruth()}. diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelper.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelper.java index 2b2bfda6928db4..628716f365bfca 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelper.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelper.java @@ -19,19 +19,34 @@ import com.google.common.base.Preconditions; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.buildtool.BuildRequestOptions; +import com.google.devtools.build.lib.cmdline.RepositoryName; +import com.google.devtools.build.lib.cmdline.TargetPattern; +import com.google.devtools.build.lib.packages.Target; +import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.SilentCloseable; +import com.google.devtools.build.lib.query2.common.UniverseScope; +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.QuerySyntaxException; +import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback; +import com.google.devtools.build.lib.query2.query.output.QueryOptions; import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption; import com.google.devtools.build.lib.runtime.ProjectFileSupport; import com.google.devtools.build.lib.runtime.events.InputFileEvent; +import com.google.devtools.build.lib.skyframe.RepositoryMappingValue.RepositoryMappingResolutionException; import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.common.options.OptionsParsingResult; import java.io.IOException; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.function.Predicate; /** Provides support for reading target patterns from a file or the command-line. */ @@ -42,20 +57,76 @@ public final class TargetPatternsHelper { private TargetPatternsHelper() {} /** - * Reads a list of target patterns, either from the command-line residue or by reading newline - * delimited target patterns from the --target_pattern_file flag. If --target_pattern_file is - * specified and options contain a residue, or if the file cannot be read, throws {@link + * Reads a list of target patterns, either from the command-line residue, by reading newline + * delimited target patterns from the --target_pattern_file flag, or from + * --target_query/--target_query_file. Command-line target patterns may be combined with + * --target_query or --target_query_file, in which case query results come first followed by + * command-line patterns. Other conflicting combinations throw {@link * TargetPatternsHelperException}. + * + * @return A list of target patterns. */ public static List readFrom(CommandEnvironment env, OptionsParsingResult options) throws TargetPatternsHelperException { List targets = options.getResidue(); BuildRequestOptions buildRequestOptions = options.getOptions(BuildRequestOptions.class); - if (!targets.isEmpty() && !buildRequestOptions.targetPatternFile.isEmpty()) { + + boolean hasQuery = !buildRequestOptions.query.isEmpty(); + boolean hasQueryFile = !buildRequestOptions.queryFile.isEmpty(); + boolean hasTargetPatternFile = !buildRequestOptions.targetPatternFile.isEmpty(); + + if (hasQuery && hasQueryFile) { + throw new TargetPatternsHelperException( + "--target_query and --target_query_file cannot both be specified", + TargetPatterns.Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN); + } + if (hasTargetPatternFile && (hasQuery || hasQueryFile)) { + throw new TargetPatternsHelperException( + "--target_pattern_file cannot be combined with --target_query or --target_query_file", + TargetPatterns.Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN); + } + if (hasTargetPatternFile && !targets.isEmpty()) { throw new TargetPatternsHelperException( "Command-line target pattern and --target_pattern_file cannot both be specified", TargetPatterns.Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN); - } else if (!buildRequestOptions.targetPatternFile.isEmpty()) { + } + + if (hasQuery || hasQueryFile) { + List queryTargets; + if (hasQuery) { + try { + queryTargets = executeQuery(env, buildRequestOptions.query, options); + } catch (QueryException | InterruptedException | IOException e) { + throw new TargetPatternsHelperException( + "Error executing query: " + e.getMessage(), + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } + } else { + Path queryFilePath = env.getWorkingDirectory().getRelative(buildRequestOptions.queryFile); + try { + env.getEventBus() + .post( + InputFileEvent.create( + /* type= */ "query_file", queryFilePath.getFileSize())); + String queryExpression = FileSystemUtils.readContent(queryFilePath, ISO_8859_1).trim(); + queryTargets = executeQuery(env, queryExpression, options); + } catch (IOException e) { + throw new TargetPatternsHelperException( + "I/O error reading from " + queryFilePath.getPathString() + ": " + e.getMessage(), + TargetPatterns.Code.TARGET_PATTERN_FILE_READ_FAILURE); + } catch (QueryException | InterruptedException e) { + throw new TargetPatternsHelperException( + "Error executing query from file: " + e.getMessage(), + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } + } + if (targets.isEmpty()) { + return queryTargets; + } + return ImmutableList.builder().addAll(queryTargets).addAll(targets).build(); + } + + if (!buildRequestOptions.targetPatternFile.isEmpty()) { // Works for absolute or relative file. Path residuePath = env.getWorkingDirectory().getRelative(buildRequestOptions.targetPatternFile); @@ -100,4 +171,68 @@ public FailureDetail getFailureDetail() { .build(); } } + + /** Executes a query and returns the resulting target patterns. */ + private static List executeQuery( + CommandEnvironment env, String queryExpression, OptionsParsingResult options) + throws QueryException, InterruptedException, IOException, TargetPatternsHelperException { + try { + var threadsOption = options.getOptions(LoadingPhaseThreadsOption.class); + var repoMapping = + env.getSkyframeExecutor() + .getMainRepoMapping(false, threadsOption.threads, env.getReporter()); + var mainRepoTargetParser = + new TargetPattern.Parser(env.getRelativeWorkingDirectory(), RepositoryName.MAIN, repoMapping); + + var starlarkSemantics = + options.getOptions(BuildLanguageOptions.class).toStarlarkSemantics(); + // Query-specific options, like --tool_deps, are not available in the 'build' command, + // so we use default options. This only affects the label printing. + var labelPrinter = + new QueryOptions().getLabelPrinter(starlarkSemantics, mainRepoTargetParser.getRepoMapping()); + + var queryEnv = + QueryEnvironmentBasedCommand.newQueryEnvironment( + env, + /* keepGoing=*/ false, + /* orderedResults= */ false, + UniverseScope.EMPTY, + threadsOption.threads, + Set.of(), + // Graphless query is sufficient since we only need target labels, not the + // dependency graph structure or ordering + /* useGraphlessQuery= */ true, + mainRepoTargetParser, + labelPrinter); + + var expr = QueryExpression.parse(queryExpression, queryEnv); + Set targetPatterns = new LinkedHashSet<>(); + var callback = + new ThreadSafeOutputFormatterCallback() { + @Override + public void processOutput(Iterable partialResult) { + for (Target target : partialResult) { + targetPatterns.add(target.getLabel().getUnambiguousCanonicalForm()); + } + } + }; + + var result = queryEnv.evaluateQuery(expr, callback); + if (!result.getSuccess()) { + throw new TargetPatternsHelperException("Query evaluation failed", + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } + + return ImmutableList.copyOf(targetPatterns); + } catch (InterruptedException e) { + throw new TargetPatternsHelperException("Query interrupted", + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } catch (RepositoryMappingResolutionException e) { + throw new TargetPatternsHelperException(e.getMessage(), + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } catch (QuerySyntaxException e) { + throw new TargetPatternsHelperException("Query syntax error: " + e.getMessage(), + TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN); + } + } } diff --git a/src/test/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelperTest.java b/src/test/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelperTest.java index acc9963380675d..931c51af86b7e8 100644 --- a/src/test/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelperTest.java +++ b/src/test/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelperTest.java @@ -140,6 +140,54 @@ public void testSpecifyNonExistingFileThrows() throws OptionsParsingException { .isEqualTo(Code.TARGET_PATTERN_FILE_READ_FAILURE); } + @Test + public void testSpecifyPatternFileAndQueryThrows() throws OptionsParsingException { + options.parse("--target_pattern_file=patterns.txt", "--target_query=deps(//...)"); + + TargetPatternsHelperException expected = + assertThrows( + TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options)); + + String message = + "--target_pattern_file cannot be combined with --target_query or --target_query_file"; + assertThat(expected).hasMessageThat().isEqualTo(message); + assertThat(expected.getFailureDetail()) + .isEqualTo( + FailureDetail.newBuilder() + .setMessage(message) + .setTargetPatterns( + TargetPatterns.newBuilder() + .setCode(Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN)) + .build()); + } + + @Test + public void testSpecifyQueryAndQueryFileThrows() throws OptionsParsingException { + options.parse("--target_query=deps(//...)", "--target_query_file=query.txt"); + + TargetPatternsHelperException expected = + assertThrows( + TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options)); + + String message = "--target_query and --target_query_file cannot both be specified"; + assertThat(expected).hasMessageThat().isEqualTo(message); + } + + @Test + public void testQueryFileWithNonExistingFileThrows() throws OptionsParsingException { + options.parse("--target_query_file=query.txt"); + + TargetPatternsHelperException expected = + assertThrows( + TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options)); + + String regex = "I/O error reading from .*query.txt.*\\(No such file or directory\\)"; + assertThat(expected).hasMessageThat().matches(regex); + assertThat(expected.getFailureDetail().hasTargetPatterns()).isTrue(); + assertThat(expected.getFailureDetail().getTargetPatterns().getCode()) + .isEqualTo(Code.TARGET_PATTERN_FILE_READ_FAILURE); + } + private static class MockEventBus extends EventBus { final Set inputFileEvents = Sets.newConcurrentHashSet(); diff --git a/src/test/shell/integration/BUILD b/src/test/shell/integration/BUILD index 87945d69f8356d..cad24e131a6c9f 100644 --- a/src/test/shell/integration/BUILD +++ b/src/test/shell/integration/BUILD @@ -113,6 +113,16 @@ sh_test( ], ) +sh_test( + name = "build_query_test", + size = "medium", + srcs = ["build_query_test.sh"], + data = [ + ":test-deps", + "@bazel_tools//tools/bash/runfiles", + ], +) + sh_test( name = "loading_phase_tests", size = "large", diff --git a/src/test/shell/integration/build_query_test.sh b/src/test/shell/integration/build_query_test.sh new file mode 100755 index 00000000000000..696e0b78ec0cde --- /dev/null +++ b/src/test/shell/integration/build_query_test.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# +# Copyright 2025 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. + +# --- begin runfiles.bash initialization --- +set -euo pipefail + +if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" +elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \ + "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" +else + echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" + exit 1 +fi +# --- end runfiles.bash initialization --- + +source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \ + || { echo "integration_test_setup.sh not found!" >&2; exit 1; } + +function set_up() { + add_rules_shell "MODULE.bazel" + mkdir -p a b c + + cat > a/BUILD <<'EOF' +filegroup( + name = "files", + srcs = ["file.txt"], + visibility = ["//visibility:public"], +) + +genrule( + name = "rule_a", + srcs = [":files"], + outs = ["output_a.txt"], + cmd = "cp $< $@", +) +EOF + + cat > a/file.txt <<'EOF' +content a +EOF + + cat > b/BUILD <<'EOF' +genrule( + name = "rule_b", + srcs = ["//a:files"], + outs = ["output_b.txt"], + cmd = "cp $< $@", +) +EOF + + cat > c/BUILD <<'EOF' +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +genrule(name = "x", outs = ["x.out"], cmd = "echo true > $@", executable = True) +sh_test(name = "test", srcs = ["x.out"]) +EOF +} + +function test_build_with_query_deps() { + bazel build --target_query="//a:rule_a" >& "$TEST_log" || fail "Build with query failed" + expect_log "//a:rule_a" + [ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built" +} + +function test_build_with_query_multiple() { + bazel build --target_query="//a:rule_a + //b:rule_b" >& "$TEST_log" || fail "Build with query failed" + [ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built" + [ -f "bazel-bin/b/output_b.txt" ] || fail "Output b/output_b.txt was not built" +} + +function test_build_with_query_pattern() { + bazel build --target_query="//a:*" >& "$TEST_log" || fail "Build with pattern query failed" + [ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built" +} + +function test_build_with_query_file() { + echo '//b:rule_b' > query.txt + + bazel build --target_query_file=query.txt >& "$TEST_log" || fail "Build with query_file failed" + expect_log "//b:rule_b" + [ -f "bazel-bin/b/output_b.txt" ] || fail "Output b/output_b.txt was not built" +} + +function test_build_with_empty_query_result() { + bazel build --target_query="//nonexistent:target" >& "$TEST_log" && fail "Should have failed with nonexistent target" + expect_log "Error executing query" +} + +function test_build_and_test_with_query() { + bazel test --target_query="tests(//c/...)" >& "$TEST_log" || fail "Should have succeeded" + expect_log "//c:test" + [ -f "bazel-bin/c/x.out" ] || fail "Output c/x.out was not built" +} + +function test_build_with_query_and_cli_targets() { + bazel build --target_query="//a:rule_a" //b:rule_b >& "$TEST_log" || fail "Build with query and CLI targets failed" + [ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built" + [ -f "bazel-bin/b/output_b.txt" ] || fail "Output b/output_b.txt was not built" +} + +run_suite "build --target_query tests" diff --git a/src/test/shell/integration/target_pattern_file_test.sh b/src/test/shell/integration/target_pattern_file_test.sh index 632a3b8edb4fea..83c6da281088a9 100755 --- a/src/test/shell/integration/target_pattern_file_test.sh +++ b/src/test/shell/integration/target_pattern_file_test.sh @@ -80,7 +80,7 @@ function test_target_pattern_file_test() { function test_target_pattern_file_and_cli_pattern() { setup bazel build --target_pattern_file=build.params -- //:x >& $TEST_log && fail "Expected failure" - expect_log "ERROR: Command-line target pattern and --target_pattern_file cannot both be specified" + expect_log "Command-line target pattern and --target_pattern_file cannot both be specified" } function test_target_pattern_file_unicode() {