Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,28 @@ 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: --query='deps(//foo) - deps(//bar)'. It is an error to specify this "
+ "along with command-line patterns or --target_pattern_file.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. It is an error to specify this along with command-line patterns, "
+ "--target_pattern_file, or --query.")
public String queryFile;

/**
* Do not use directly. Instead use {@link
* com.google.devtools.build.lib.runtime.CommandEnvironment#withMergedAnalysisAndExecutionSourceOfTruth()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,41 @@
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.cmdline.TargetPattern;
import com.google.devtools.build.lib.packages.LabelPrinter;
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.AbstractBlazeQueryEnvironment;
import com.google.devtools.build.lib.query2.common.UniverseScope;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
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.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.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import net.starlark.java.eval.StarlarkSemantics;

/** Provides support for reading target patterns from a file or the command-line. */
public final class TargetPatternsHelper {
Expand All @@ -42,20 +64,58 @@ 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
* TargetPatternsHelperException}.
* 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.
* If multiple options are specified, throws {@link TargetPatternsHelperException}.
*
* @return A list of target patterns.
*/
public static List<String> readFrom(CommandEnvironment env, OptionsParsingResult options)
throws TargetPatternsHelperException {
List<String> targets = options.getResidue();
BuildRequestOptions buildRequestOptions = options.getOptions(BuildRequestOptions.class);
if (!targets.isEmpty() && !buildRequestOptions.targetPatternFile.isEmpty()) {

int optionCount = 0;
if (!targets.isEmpty()) optionCount++;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google style dislikes single line ifs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a design reason that residue targets can't be mixed with a query?
I am specifically interested in how it interacts with --remote_download_outputs=toplevel, as that is one place where we use a query like this to download only certain files, while still building everything:

bazel test --remote_download_outputs=top-level //... $(bazel query 'attr(tags, ci-download-outputs, //...)')

I haven't figured out if there is an existing technical limitation that would make supporting that difficult.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing inherent, i inherited this logic from targetPatternFile and I haven't traced back why it was done for that case

if (!buildRequestOptions.targetPatternFile.isEmpty()) optionCount++;
if (!buildRequestOptions.query.isEmpty()) optionCount++;
if (!buildRequestOptions.queryFile.isEmpty()) optionCount++;
if (optionCount > 1) {
throw new TargetPatternsHelperException(
"Command-line target pattern and --target_pattern_file cannot both be specified",
"Only one of command-line target patterns, --target_pattern_file, --target_query, "
+ "or --target_query_file may be specified",
TargetPatterns.Code.TARGET_PATTERN_FILE_WITH_COMMAND_LINE_PATTERN);
} else if (!buildRequestOptions.targetPatternFile.isEmpty()) {
}

if (!buildRequestOptions.query.isEmpty()) {
try {
return executeQuery(env, buildRequestOptions.query, options);
} catch (QueryException | InterruptedException | IOException e) {
throw new TargetPatternsHelperException(
"Error executing query: " + e.getMessage(),
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk what exit codes i should really use here, i didn't want to force all callers to handle this same logic, so I kept only this 1 exception type that they're already handling

}
} else if (!buildRequestOptions.queryFile.isEmpty()) {
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();
return 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 (!buildRequestOptions.targetPatternFile.isEmpty()) {
// Works for absolute or relative file.
Path residuePath =
env.getWorkingDirectory().getRelative(buildRequestOptions.targetPatternFile);
Expand Down Expand Up @@ -100,4 +160,64 @@ public FailureDetail getFailureDetail() {
.build();
}
}

/** Executes a query and returns the resulting target patterns. */
private static List<String> executeQuery(
CommandEnvironment env, String queryExpression, OptionsParsingResult options)
throws QueryException, InterruptedException, IOException, TargetPatternsHelperException {
try {
LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class);
RepositoryMapping repoMapping =
env.getSkyframeExecutor()
.getMainRepoMapping(false, threadsOption.threads, env.getReporter());
TargetPattern.Parser mainRepoTargetParser =
new TargetPattern.Parser(env.getRelativeWorkingDirectory(), RepositoryName.MAIN, repoMapping);

StarlarkSemantics starlarkSemantics =
options.getOptions(BuildLanguageOptions.class).toStarlarkSemantics();
LabelPrinter labelPrinter =
new QueryOptions().getLabelPrinter(starlarkSemantics, mainRepoTargetParser.getRepoMapping());

AbstractBlazeQueryEnvironment<Target> queryEnv =
QueryEnvironmentBasedCommand.newQueryEnvironment(
env,
/* keepGoing=*/ false,
/* orderedResults= */ false,
UniverseScope.EMPTY,
threadsOption.threads,
Set.of(),
/* useGraphlessQuery= */ true,
mainRepoTargetParser,
labelPrinter);

QueryExpression expr = QueryExpression.parse(queryExpression, queryEnv);
Set<String> targetPatterns = new LinkedHashSet<>();
ThreadSafeOutputFormatterCallback<Target> callback =
new ThreadSafeOutputFormatterCallback<Target>() {
@Override
public void processOutput(Iterable<Target> partialResult) {
for (Target target : partialResult) {
targetPatterns.add(target.getLabel().toString());
}
}
};

QueryEvalResult result = queryEnv.evaluateQuery(expr, callback);
if (!result.getSuccess()) {
throw new TargetPatternsHelperException("Query evaluation failed",
TargetPatterns.Code.TARGET_PATTERNS_UNKNOWN);
}

return new ArrayList<>(targetPatterns);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer ImmutableList.copyOf here and make the return type more specific.

} 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ public void testSpecifyPatternAndFileThrows() throws OptionsParsingException {
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));

String message =
"Command-line target pattern and --target_pattern_file cannot both be specified";
"Only one of command-line target patterns, --target_pattern_file, --target_query, "
+ "or --target_query_file may be specified";
assertThat(expected).hasMessageThat().isEqualTo(message);
assertThat(expected.getFailureDetail())
.isEqualTo(
Expand Down Expand Up @@ -140,6 +141,58 @@ public void testSpecifyNonExistingFileThrows() throws OptionsParsingException {
.isEqualTo(Code.TARGET_PATTERN_FILE_READ_FAILURE);
}

@Test
public void testSpecifyMultipleOptionsThrows() throws OptionsParsingException {
options.parse("--target_pattern_file=patterns.txt", "--target_query=deps(//...)");

TargetPatternsHelperException expected =
assertThrows(
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));

String message =
"Only one of command-line target patterns, --target_pattern_file, --target_query, "
+ "or --target_query_file may be specified";
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 testSpecifyQueryAndPatternThrows() throws OptionsParsingException {
options.parse("--target_query=deps(//...)");
options.setResidue(ImmutableList.of("//some:pattern"), ImmutableList.of());

TargetPatternsHelperException expected =
assertThrows(
TargetPatternsHelperException.class, () -> TargetPatternsHelper.readFrom(env, options));

String message =
"Only one of command-line target patterns, --target_pattern_file, --target_query, "
+ "or --target_query_file may 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<InputFileEvent> inputFileEvents = Sets.newConcurrentHashSet();

Expand Down
10 changes: 10 additions & 0 deletions src/test/shell/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading