Skip to content

Commit

Permalink
Support cquery Starlark expression output formatter
Browse files Browse the repository at this point in the history
This allows the user to specify a Starlark expression to be evaluated (and printed as output) for each configured target in the cquery result.

The configured target may be referenced in the Starlark expression via `target`.

Example usage:
$ bazel cquery --output=starlark --expr='" ".join(target.files.to_list())' //foo

RELNOTES: None.
PiperOrigin-RevId: 327813810
  • Loading branch information
c-parsons authored and copybara-github committed Aug 21, 2020
1 parent bcabfd7 commit 487d0b2
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 3 deletions.
54 changes: 53 additions & 1 deletion site/docs/cquery.html
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ <h3> Transitions </h3>
without the options diff.
</p>

<h3>Proto</h3>
<h3>Protocol message output</h3>

<pre>
--output=proto
Expand Down Expand Up @@ -401,6 +401,58 @@ <h4>--[no]proto:include_configurations</h4>
included as <code>rule_input</code> fields.
</p>

<h3>Formatting output with a Starlark expression</h3>

<pre>
--output=starlark
</pre>

<p>
This output format evaluates and prints a given
<a href="https://docs.bazel.build/versions/master/skylark/language.html">Starlark</a>
expression for each resulting target. The expression may be specified with the
<code>--starlark:expr</code> flag.
</p><p>
The specified expression can access only the core Starlark
<a href="https://github.com/bazelbuild/starlark/blob/master/spec.md#built-in-constants-and-functions">
built-in constants and functions</a>, and a global <code>target</code>, of type
<a href="https://docs.bazel.build/versions/master/skylark/lib/Target.html">Target</a>.
Note it does <b>not</b> have access to many of the
additional built-in constants and functions supported for rule/macro code, such as
<code>glob()</code> or <code>rule()</code>
</p>
<h4>Examples</h4>

<p>
Print a space-separated list of the base names of all files produced by <code>//foo</code>:
</p>
<pre>
bazel cquery //foo --output=starlark \
--starlark:expr="' '.join([f.basename for f in target.files.to_list()])"
</pre>
<p>
Print a space-separated list of the paths of all files produced by <b>rule</b> targets in
<code>//bar</code> and its subpackages:
</p>
<pre>
bazel cquery 'kind(rule, //bar/...)' --output=starlark \
--starlark:expr="' '.join([f.path for f in target.files.to_list()])"
</pre>
<p>
Print a list of the mnemonics of all actions registered by <code>//foo</code>.
</p>
<pre>
bazel cquery //foo --output=starlark \
--starlark:expr="[a.mnemonic for a in target.actions]"
</pre>
<p>
Print a list of compilation outputs registered by a <code>cc_library</code> <code>//baz</code>.
</p>
<pre>
bazel cquery //baz --output=starlark \
--starlark:expr="[f.path for f in target.output_groups.compilation_outputs.to_list()]"
</pre>

<h2 id='compare'>cquery vs. query</h2>

<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ public PostAnalysisQueryEnvironment(
SkyframeExecutor skyframeExecutor,
BuildConfiguration hostConfiguration,
@Nullable TransitionFactory<Rule> trimmingTransitionFactory,
PackageManager packageManager);
PackageManager packageManager)
throws QueryException, InterruptedException;

public abstract String getOutputFormat();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ private static ImmutableMap<String, BuildConfiguration> getTransitiveConfigurati
SkyframeExecutor skyframeExecutor,
BuildConfiguration hostConfiguration,
@Nullable TransitionFactory<Rule> trimmingTransitionFactory,
PackageManager packageManager) {
PackageManager packageManager)
throws QueryException, InterruptedException {
AspectResolver aspectResolver =
cqueryOptions.aspectDeps.createResolver(packageManager, eventHandler);
return ImmutableList.of(
Expand Down Expand Up @@ -247,6 +248,8 @@ private static ImmutableMap<String, BuildConfiguration> getTransitiveConfigurati
aspectResolver,
OutputType.JSON),
new BuildOutputFormatterCallback(
eventHandler, cqueryOptions, out, skyframeExecutor, accessor),
new StarlarkOutputFormatterCallback(
eventHandler, cqueryOptions, out, skyframeExecutor, accessor));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,14 @@ public enum Transitions {
"if enabled, proto output will include information about configurations. When disabled,"
+ "cquery proto output format resembles query output format.")
public boolean protoIncludeConfigurations;

@Option(
name = "starlark:expr",
defaultValue = "str(target.label)",
documentationCategory = OptionDocumentationCategory.QUERY,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help =
"A Starlark expression to format each configured target in cquery's"
+ " --output=starlark mode. The configured target is bound to 'target'.")
public String expr;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2020 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.query2.cquery;

import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.TargetAccessor;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.server.FailureDetails.ConfigurableQuery.Code;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
import com.google.devtools.build.lib.syntax.Expression;
import com.google.devtools.build.lib.syntax.FileOptions;
import com.google.devtools.build.lib.syntax.Module;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInput;
import com.google.devtools.build.lib.syntax.Starlark;
import com.google.devtools.build.lib.syntax.StarlarkFunction;
import com.google.devtools.build.lib.syntax.StarlarkSemantics;
import com.google.devtools.build.lib.syntax.StarlarkThread;
import com.google.devtools.build.lib.syntax.SyntaxError;
import java.io.OutputStream;

/**
* Starlark output formatter for cquery results. Each configured target will result in an evaluation
* of the Starlark expression specified by {@code --expr}.
*/
public class StarlarkOutputFormatterCallback extends CqueryThreadsafeCallback {
private static final Object[] NO_ARGS = new Object[0];

// Starlark function with single required parameter "target", a ConfiguredTarget query result.
private final StarlarkFunction exprEvalFn;

StarlarkOutputFormatterCallback(
ExtendedEventHandler eventHandler,
CqueryOptions options,
OutputStream out,
SkyframeExecutor skyframeExecutor,
TargetAccessor<ConfiguredTarget> accessor)
throws QueryException, InterruptedException {
super(eventHandler, options, out, skyframeExecutor, accessor);

// Validate that options.expr is a pure expression (for example, that it does not attempt
// to escape its scope via unbalanced parens).
ParserInput exprParserInput = ParserInput.fromString(options.expr, "--starlark:expr");
try {
Expression.parse(exprParserInput);
} catch (SyntaxError.Exception ex) {
throw new QueryException(
"invalid --starlark:expr: " + ex.getMessage(), Code.STARLARK_SYNTAX_ERROR);
}

// Create a synthetic file that defines a function with single parameter "target",
// whose body is provided by the user's expression.
String fileBody = "def f(target): return (" + options.expr + ")\n" + "f";
ParserInput input = ParserInput.fromString(fileBody, "--starlark:expr");

try {
StarlarkThread thread =
new StarlarkThread(Mutability.create("foo"), StarlarkSemantics.DEFAULT);
this.exprEvalFn =
(StarlarkFunction) EvalUtils.exec(input, FileOptions.DEFAULT, Module.create(), thread);
} catch (SyntaxError.Exception ex) {
throw new QueryException(
"invalid --starlark:expr: " + ex.getMessage(), Code.STARLARK_SYNTAX_ERROR);
} catch (EvalException ex) {
throw new QueryException(
"invalid --starlark:expr: " + ex.getMessageWithStack(), Code.STARLARK_EVAL_ERROR);
}
}

@Override
public String getName() {
return "starlark";
}

@Override
public void processOutput(Iterable<ConfiguredTarget> partialResult) throws InterruptedException {
StarlarkThread thread =
new StarlarkThread(Mutability.create("cquery evaluation"), StarlarkSemantics.DEFAULT);
thread.setMaxExecutionSteps(500_000L);

for (ConfiguredTarget target : partialResult) {
try {
// Invoke exprEvalFn with `target` argument.
Object result = Starlark.fastcall(thread, this.exprEvalFn, new Object[] {target}, NO_ARGS);

addResult(Starlark.str(result));
} catch (EvalException ex) {
eventHandler.handle(
Event.error(
String.format(
"Starlark evaluation error for %s: %s",
target.getLabel(), ex.getMessageWithStack())));
continue;
}
}
}
}
2 changes: 2 additions & 0 deletions src/main/protobuf/failure_details.proto
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,8 @@ message ConfigurableQuery {
ATTRIBUTE_MISSING = 7 [(metadata) = { exit_code: 2 }];
INCORRECT_CONFIG_ARGUMENT_ERROR = 8 [(metadata) = { exit_code: 2 }];
TARGET_MISSING = 9 [(metadata) = { exit_code: 2 }];
STARLARK_SYNTAX_ERROR = 10 [(metadata) = { exit_code: 2 }];
STARLARK_EVAL_ERROR = 11 [(metadata) = { exit_code: 2 }];
}

Code code = 1;
Expand Down
85 changes: 85 additions & 0 deletions src/test/shell/integration/configured_query_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -810,4 +810,89 @@ EOF
assert_equals "$(grep some_file output | wc -l | egrep -o '[0-9]+')" "1"
}

function test_starlark_output_mode() {
local -r pkg=$FUNCNAME
mkdir -p $pkg
cat > $pkg/BUILD <<EOF
py_library(
name = "pylib",
srcs = ["pylib.py"],
)
py_library(
name = "pylibtwo",
srcs = ["pylibtwo.py", "pylibtwo2.py",],
)
EOF

bazel cquery "//$pkg:all" --output=starlark \
--starlark:expr="str(target.label) + '%foo'" > output \
2>"$TEST_log" || fail "Expected success"

assert_contains "//$pkg:pylib%foo" output
assert_contains "//$pkg:pylibtwo%foo" output

bazel cquery "//$pkg:all" --output=starlark \
--starlark:expr="str(target.label) + '%' + str(target.files.to_list()[1].is_directory)" \
> output 2>"$TEST_log" || fail "Expected success"

assert_contains "//$pkg:pylibtwo%False" output
# pylib evaluation will fail, as it has only one output file.
assert_contains "Starlark evaluation error for //$pkg:pylib" "$TEST_log"
}

function test_starlark_output_invalid_expression() {
local -r pkg=$FUNCNAME
mkdir -p $pkg
touch $pkg/BUILD

bazel cquery "//$pkg:all" --output=starlark \
--starlark:expr="no_symbol" \
> output 2>"$TEST_log" && fail "Expected failure"

assert_contains "invalid --starlark:expr: name 'no_symbol' is not defined" $TEST_log

bazel cquery "//$pkg:all" --output=starlark \
--starlark:expr="def foo(): return 5" \
> output 2>"$TEST_log" && fail "Expected failure"

assert_contains "syntax error at 'def': expected expression" $TEST_log
}

function test_starlark_output_cc_library_files() {
local -r pkg=$FUNCNAME
mkdir -p $pkg
cat > $pkg/BUILD <<EOF
cc_library(
name = "cclib",
srcs = ["mylib.cc"],
)
EOF

bazel cquery "//$pkg:all" --output=starlark \
--starlark:expr="' '.join([f.basename for f in target.files.to_list()])" \
> output 2>"$TEST_log" || fail "Expected failure"

if "$is_windows"; then
assert_contains "cclib.lib" output
else
assert_contains "libcclib.a" output
assert_contains "libcclib.so" output
fi
}

function test_starlark_file_output() {
local -r pkg=$FUNCNAME
mkdir -p $pkg
cat > $pkg/BUILD <<EOF
exports_files(srcs = ["foo"])
EOF

bazel cquery "//$pkg:foo" --output=starlark \
--starlark:expr="'path=' + target.files.to_list()[0].path" \
> output 2>"$TEST_log" || fail "Expected failure"

assert_contains "^path=$pkg/foo$" output
}

run_suite "${PRODUCT_NAME} configured query tests"

0 comments on commit 487d0b2

Please sign in to comment.