Skip to content

Commit 4307f8c

Browse files
committed
Add support for 'build --query'
This allows combining the common pattern of build and then query into a single build command that executes the passed query (or query_file) Fixes #26938 RELNOTES[inc]: Add 'build --query' flag for building the result of a bazel query in a single command
1 parent 820ad5d commit 4307f8c

File tree

5 files changed

+319
-8
lines changed

5 files changed

+319
-8
lines changed

src/main/java/com/google/devtools/build/lib/buildtool/BuildRequestOptions.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,28 @@ public String getSymlinkPrefix(String productName) {
366366
+ "line. It is an error to specify a file here as well as command-line patterns.")
367367
public String targetPatternFile;
368368

369+
@Option(
370+
name = "query",
371+
defaultValue = "",
372+
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
373+
effectTags = {OptionEffectTag.CHANGES_INPUTS},
374+
help =
375+
"If set, build will evaluate the query expression and build the resulting targets. "
376+
+ "Example: --query='deps(//foo) - deps(//bar)'. It is an error to specify this "
377+
+ "along with command-line patterns or --target_pattern_file.")
378+
public String query;
379+
380+
@Option(
381+
name = "query_file",
382+
defaultValue = "",
383+
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
384+
effectTags = {OptionEffectTag.CHANGES_INPUTS},
385+
help =
386+
"If set, build will read a query expression from the file named here and build the "
387+
+ "resulting targets. It is an error to specify this along with command-line patterns, "
388+
+ "--target_pattern_file, or --query.")
389+
public String queryFile;
390+
369391
/**
370392
* Do not use directly. Instead use {@link
371393
* com.google.devtools.build.lib.runtime.CommandEnvironment#withMergedAnalysisAndExecutionSourceOfTruth()}.

src/main/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelper.java

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,41 @@
2020
import com.google.common.base.Preconditions;
2121
import com.google.common.base.Splitter;
2222
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
23+
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
24+
import com.google.devtools.build.lib.cmdline.RepositoryName;
25+
import com.google.devtools.build.lib.cmdline.TargetPattern;
26+
import com.google.devtools.build.lib.packages.LabelPrinter;
27+
import com.google.devtools.build.lib.packages.Target;
28+
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
2329
import com.google.devtools.build.lib.profiler.Profiler;
2430
import com.google.devtools.build.lib.profiler.SilentCloseable;
31+
import com.google.devtools.build.lib.query2.common.AbstractBlazeQueryEnvironment;
32+
import com.google.devtools.build.lib.query2.common.UniverseScope;
33+
import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
34+
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
35+
import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
36+
import com.google.devtools.build.lib.query2.engine.QueryException;
37+
import com.google.devtools.build.lib.query2.engine.QueryExpression;
38+
import com.google.devtools.build.lib.query2.engine.QuerySyntaxException;
39+
import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback;
40+
import com.google.devtools.build.lib.query2.query.output.QueryOptions;
2541
import com.google.devtools.build.lib.runtime.CommandEnvironment;
42+
import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption;
2643
import com.google.devtools.build.lib.runtime.ProjectFileSupport;
2744
import com.google.devtools.build.lib.runtime.events.InputFileEvent;
45+
import com.google.devtools.build.lib.skyframe.RepositoryMappingValue.RepositoryMappingResolutionException;
2846
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
2947
import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
3048
import com.google.devtools.build.lib.vfs.FileSystemUtils;
3149
import com.google.devtools.build.lib.vfs.Path;
3250
import com.google.devtools.common.options.OptionsParsingResult;
3351
import java.io.IOException;
52+
import java.util.ArrayList;
53+
import java.util.LinkedHashSet;
3454
import java.util.List;
55+
import java.util.Set;
3556
import java.util.function.Predicate;
57+
import net.starlark.java.eval.StarlarkSemantics;
3658

3759
/** Provides support for reading target patterns from a file or the command-line. */
3860
public final class TargetPatternsHelper {
@@ -42,20 +64,58 @@ public final class TargetPatternsHelper {
4264
private TargetPatternsHelper() {}
4365

4466
/**
45-
* Reads a list of target patterns, either from the command-line residue or by reading newline
46-
* delimited target patterns from the --target_pattern_file flag. If --target_pattern_file is
47-
* specified and options contain a residue, or if the file cannot be read, throws {@link
48-
* TargetPatternsHelperException}.
67+
* Reads a list of target patterns, either from the command-line residue, by reading newline
68+
* delimited target patterns from the --target_pattern_file flag, or from --query/--query_file.
69+
* If multiple options are specified, throws {@link TargetPatternsHelperException}.
70+
*
71+
* @return A list of target patterns.
4972
*/
5073
public static List<String> readFrom(CommandEnvironment env, OptionsParsingResult options)
5174
throws TargetPatternsHelperException {
5275
List<String> targets = options.getResidue();
5376
BuildRequestOptions buildRequestOptions = options.getOptions(BuildRequestOptions.class);
54-
if (!targets.isEmpty() && !buildRequestOptions.targetPatternFile.isEmpty()) {
77+
78+
int optionCount = 0;
79+
if (!targets.isEmpty()) optionCount++;
80+
if (!buildRequestOptions.targetPatternFile.isEmpty()) optionCount++;
81+
if (!buildRequestOptions.query.isEmpty()) optionCount++;
82+
if (!buildRequestOptions.queryFile.isEmpty()) optionCount++;
83+
if (optionCount > 1) {
5584
throw new TargetPatternsHelperException(
56-
"Command-line target pattern and --target_pattern_file cannot both be specified",
85+
"Only one of command-line target patterns, --target_pattern_file, --query, "
86+
+ "or --query_file may be specified",
5787
TargetPatterns.Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN);
58-
} else if (!buildRequestOptions.targetPatternFile.isEmpty()) {
88+
}
89+
90+
if (!buildRequestOptions.query.isEmpty()) {
91+
try {
92+
return executeQuery(env, buildRequestOptions.query, options);
93+
} catch (QueryException | InterruptedException | IOException e) {
94+
throw new TargetPatternsHelperException(
95+
"Error executing query: " + e.getMessage(),
96+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
97+
}
98+
} else if (!buildRequestOptions.queryFile.isEmpty()) {
99+
Path queryFilePath = env.getWorkingDirectory().getRelative(buildRequestOptions.queryFile);
100+
try {
101+
env.getEventBus()
102+
.post(
103+
InputFileEvent.create(
104+
/* type= */ "query_file", queryFilePath.getFileSize()));
105+
String queryExpression = FileSystemUtils.readContent(queryFilePath, ISO_8859_1).trim();
106+
return executeQuery(env, queryExpression, options);
107+
} catch (IOException e) {
108+
throw new TargetPatternsHelperException(
109+
"I/O error reading from " + queryFilePath.getPathString() + ": " + e.getMessage(),
110+
TargetPatterns.Code.TARGET_PATTERN_FILE_READ_FAILURE);
111+
} catch (QueryException | InterruptedException e) {
112+
throw new TargetPatternsHelperException(
113+
"Error executing query from file: " + e.getMessage(),
114+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
115+
}
116+
}
117+
118+
if (!buildRequestOptions.targetPatternFile.isEmpty()) {
59119
// Works for absolute or relative file.
60120
Path residuePath =
61121
env.getWorkingDirectory().getRelative(buildRequestOptions.targetPatternFile);
@@ -100,4 +160,64 @@ public FailureDetail getFailureDetail() {
100160
.build();
101161
}
102162
}
163+
164+
/** Executes a query and returns the resulting target patterns. */
165+
private static List<String> executeQuery(
166+
CommandEnvironment env, String queryExpression, OptionsParsingResult options)
167+
throws QueryException, InterruptedException, IOException, TargetPatternsHelperException {
168+
try {
169+
LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class);
170+
RepositoryMapping repoMapping =
171+
env.getSkyframeExecutor()
172+
.getMainRepoMapping(false, threadsOption.threads, env.getReporter());
173+
TargetPattern.Parser mainRepoTargetParser =
174+
new TargetPattern.Parser(env.getRelativeWorkingDirectory(), RepositoryName.MAIN, repoMapping);
175+
176+
StarlarkSemantics starlarkSemantics =
177+
options.getOptions(BuildLanguageOptions.class).toStarlarkSemantics();
178+
LabelPrinter labelPrinter =
179+
new QueryOptions().getLabelPrinter(starlarkSemantics, mainRepoTargetParser.getRepoMapping());
180+
181+
AbstractBlazeQueryEnvironment<Target> queryEnv =
182+
QueryEnvironmentBasedCommand.newQueryEnvironment(
183+
env,
184+
/* keepGoing=*/ false,
185+
/* orderedResults= */ false,
186+
UniverseScope.EMPTY,
187+
threadsOption.threads,
188+
Set.of(),
189+
/* useGraphlessQuery= */ true,
190+
mainRepoTargetParser,
191+
labelPrinter);
192+
193+
QueryExpression expr = QueryExpression.parse(queryExpression, queryEnv);
194+
Set<String> targetPatterns = new LinkedHashSet<>();
195+
ThreadSafeOutputFormatterCallback<Target> callback =
196+
new ThreadSafeOutputFormatterCallback<Target>() {
197+
@Override
198+
public void processOutput(Iterable<Target> partialResult) {
199+
for (Target target : partialResult) {
200+
targetPatterns.add(target.getLabel().toString());
201+
}
202+
}
203+
};
204+
205+
QueryEvalResult result = queryEnv.evaluateQuery(expr, callback);
206+
if (!result.getSuccess()) {
207+
throw new TargetPatternsHelperException("Query evaluation failed",
208+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
209+
}
210+
211+
return new ArrayList<>(targetPatterns);
212+
} catch (InterruptedException e) {
213+
throw new TargetPatternsHelperException("Query interrupted",
214+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
215+
} catch (RepositoryMappingResolutionException e) {
216+
throw new TargetPatternsHelperException(e.getMessage(),
217+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
218+
} catch (QuerySyntaxException e) {
219+
throw new TargetPatternsHelperException("Query syntax error: " + e.getMessage(),
220+
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
221+
}
222+
}
103223
}

src/test/java/com/google/devtools/build/lib/runtime/commands/TargetPatternsHelperTest.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public void testSpecifyPatternAndFileThrows() throws OptionsParsingException {
112112
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));
113113

114114
String message =
115-
"Command-line target pattern and --target_pattern_file cannot both be specified";
115+
"Only one of command-line target patterns, --target_pattern_file, --query, "
116+
+ "or --query_file may be specified";
116117
assertThat(expected).hasMessageThat().isEqualTo(message);
117118
assertThat(expected.getFailureDetail())
118119
.isEqualTo(
@@ -140,6 +141,58 @@ public void testSpecifyNonExistingFileThrows() throws OptionsParsingException {
140141
.isEqualTo(Code.TARGET_PATTERN_FILE_READ_FAILURE);
141142
}
142143

144+
@Test
145+
public void testSpecifyMultipleOptionsThrows() throws OptionsParsingException {
146+
options.parse("--target_pattern_file=patterns.txt", "--query=deps(//...)");
147+
148+
TargetPatternsHelperException expected =
149+
assertThrows(
150+
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));
151+
152+
String message =
153+
"Only one of command-line target patterns, --target_pattern_file, --query, "
154+
+ "or --query_file may be specified";
155+
assertThat(expected).hasMessageThat().isEqualTo(message);
156+
assertThat(expected.getFailureDetail())
157+
.isEqualTo(
158+
FailureDetail.newBuilder()
159+
.setMessage(message)
160+
.setTargetPatterns(
161+
TargetPatterns.newBuilder()
162+
.setCode(Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN))
163+
.build());
164+
}
165+
166+
@Test
167+
public void testSpecifyQueryAndPatternThrows() throws OptionsParsingException {
168+
options.parse("--query=deps(//...)");
169+
options.setResidue(ImmutableList.of("//some:pattern"), ImmutableList.of());
170+
171+
TargetPatternsHelperException expected =
172+
assertThrows(
173+
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));
174+
175+
String message =
176+
"Only one of command-line target patterns, --target_pattern_file, --query, "
177+
+ "or --query_file may be specified";
178+
assertThat(expected).hasMessageThat().isEqualTo(message);
179+
}
180+
181+
@Test
182+
public void testQueryFileWithNonExistingFileThrows() throws OptionsParsingException {
183+
options.parse("--query_file=query.txt");
184+
185+
TargetPatternsHelperException expected =
186+
assertThrows(
187+
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));
188+
189+
String regex = "I/O error reading from .*query.txt.*\\(No such file or directory\\)";
190+
assertThat(expected).hasMessageThat().matches(regex);
191+
assertThat(expected.getFailureDetail().hasTargetPatterns()).isTrue();
192+
assertThat(expected.getFailureDetail().getTargetPatterns().getCode())
193+
.isEqualTo(Code.TARGET_PATTERN_FILE_READ_FAILURE);
194+
}
195+
143196
private static class MockEventBus extends EventBus {
144197
final Set<InputFileEvent> inputFileEvents = Sets.newConcurrentHashSet();
145198

src/test/shell/integration/BUILD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ sh_test(
113113
],
114114
)
115115

116+
sh_test(
117+
name = "build_query_test",
118+
size = "medium",
119+
srcs = ["build_query_test.sh"],
120+
data = [
121+
":test-deps",
122+
"@bazel_tools//tools/bash/runfiles",
123+
],
124+
)
125+
116126
sh_test(
117127
name = "loading_phase_tests",
118128
size = "large",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright 2025 The Bazel Authors. All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
# --- begin runfiles.bash initialization ---
18+
set -euo pipefail
19+
20+
if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
21+
if [[ -f "$0.runfiles_manifest" ]]; then
22+
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
23+
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
24+
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
25+
elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
26+
export RUNFILES_DIR="$0.runfiles"
27+
fi
28+
fi
29+
if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
30+
source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
31+
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
32+
source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
33+
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
34+
else
35+
echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
36+
exit 1
37+
fi
38+
# --- end runfiles.bash initialization ---
39+
40+
source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
41+
|| { echo "integration_test_setup.sh not found!" >&2; exit 1; }
42+
43+
function set_up() {
44+
touch MODULE.bazel
45+
mkdir -p a b
46+
47+
cat > a/BUILD <<'EOF'
48+
filegroup(
49+
name = "files",
50+
srcs = ["file.txt"],
51+
visibility = ["//visibility:public"],
52+
)
53+
54+
genrule(
55+
name = "rule_a",
56+
srcs = [":files"],
57+
outs = ["output_a.txt"],
58+
cmd = "cp $< $@",
59+
)
60+
EOF
61+
62+
cat > a/file.txt <<'EOF'
63+
content a
64+
EOF
65+
66+
cat > b/BUILD <<'EOF'
67+
genrule(
68+
name = "rule_b",
69+
srcs = ["//a:files"],
70+
outs = ["output_b.txt"],
71+
cmd = "cp $< $@",
72+
)
73+
EOF
74+
}
75+
76+
function test_build_with_query_deps() {
77+
bazel build --query="//a:rule_a" >& "$TEST_log" || fail "Build with query failed"
78+
expect_log "//a:rule_a"
79+
[ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built"
80+
}
81+
82+
function test_build_with_query_multiple() {
83+
bazel build --query="//a:rule_a + //b:rule_b" >& "$TEST_log" || fail "Build with query failed"
84+
[ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built"
85+
[ -f "bazel-bin/b/output_b.txt" ] || fail "Output b/output_b.txt was not built"
86+
}
87+
88+
function test_build_with_query_pattern() {
89+
bazel build --query="//a:*" >& "$TEST_log" || fail "Build with pattern query failed"
90+
[ -f "bazel-bin/a/output_a.txt" ] || fail "Output a/output_a.txt was not built"
91+
}
92+
93+
function test_build_with_query_file() {
94+
echo '//b:rule_b' > query.txt
95+
96+
bazel build --query_file=query.txt >& "$TEST_log" || fail "Build with query_file failed"
97+
expect_log "//b:rule_b"
98+
[ -f "bazel-bin/b/output_b.txt" ] || fail "Output b/output_b.txt was not built"
99+
}
100+
101+
function test_build_with_empty_query_result() {
102+
bazel build --query="//nonexistent:target" >& "$TEST_log" && fail "Should have failed with nonexistent target"
103+
expect_log "Error executing query"
104+
}
105+
106+
run_suite "build --query tests"

0 commit comments

Comments
 (0)