Skip to content

Commit

Permalink
Add examples of selectable copts and test wrapper (bazelbuild#158)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliexxia authored May 19, 2020
1 parent 0961d42 commit 2143d40
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ tasks:
working_directory: rules
build_targets:
- "..."
# These targest are not supposed to build at the top level without flags being set
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features"
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature1_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature2_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:lib"
# TODO(#160) renable this target when split_attr logic is released
- "-//starlark_configurations/multi_arch_binary:foo"
test_targets:
Expand All @@ -150,6 +156,12 @@ tasks:
working_directory: rules
build_targets:
- "..."
# These targest are not supposed to build at the top level without flags being set
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features"
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature1_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature2_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:lib"
# TODO(#160) renable this target when split_attr logic is released
- "-//starlark_configurations/multi_arch_binary:foo"
test_targets:
Expand All @@ -160,6 +172,12 @@ tasks:
working_directory: rules
build_targets:
- "..."
# These targest are not supposed to build at the top level without flags being set
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features"
- "-//starlark_configurations/cc_binary_selectable_copts:app_forgets_to_set_features_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature1_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:app_with_feature2_native_binary"
- "-//starlark_configurations/cc_binary_selectable_copts:lib"
# TODO(#160) renable this target when split_attr logic is released
- "-//starlark_configurations/multi_arch_binary:foo"
# TODO(bazel-team): Make runfiles examples work on Windows.
Expand Down
41 changes: 41 additions & 0 deletions rules/starlark_configurations/cc_binary_selectable_copts/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This load statement replaces the native cc_binary (which has no "set_features"
# attribute) with a macro that strings together the logic to make that work,
# then passes everything else back to the native cc_binary.
load(":defs.bzl", "transition_rule", "cc_binary")

# To see what this does, try "$ bazel run //:app_with_feature1".
cc_binary(
name = "app_with_feature1",
srcs = ["main.cc"],
set_features = "feature1",
deps = [":lib"],
)

# To see what this does, try "$ bazel run //:app_with_feature2".
cc_binary(
name = "app_with_feature2",
srcs = ["main.cc"],
set_features = "feature2",
deps = [":lib"],
)

# This binary "forgets" to set any features, so we have it fail with
# a descriptive message.
cc_binary(
name = "app_forgets_to_set_features",
srcs = ["main.cc"],
deps = [":lib"],
)

# The library only builds if some feature is requested.
cc_library(
name = "lib",
srcs = ["lib.cc"],
copts = select(
{
"//starlark_configurations/cc_binary_selectable_copts/custom_settings:feature1": ["-Dfeature1"],
"//starlark_configurations/cc_binary_selectable_copts/custom_settings:feature2": ["-Dfeature2"],
},
no_match_error = "You must explicitly set which features you want!",
),
)
66 changes: 66 additions & 0 deletions rules/starlark_configurations/cc_binary_selectable_copts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
### Example showing how to use [Starlark configuration](https://docs.bazel.build/versions/master/skylark/config.html) so a `cc_binary` can set custom "features" for `cc_library` dependencies to include.


This example has five files:

* [custom_settings/BUILD](custom_settings/BUILD) - This defines a [custom
Starlark
flag](https://docs.bazel.build/versions/master/skylark/config.html#user-defined-build-settings)
called `//custom_settings:mycopts` which defines the set of possible features
and stores whatever features the `cc_binary` sets.

`cc_library` uses a `select` to read this flag and set its `copts`
accordingly. This file also declares the `config_setting`s the `select` uses.

* [defs.bzl](defs.bzl) - This defines a custom Starlark rule `transition_rule`
which defines an attribute `set_features` that sets the desired feature and
`actual_binary` which declares the `cc_binary` this should apply to.

Because `cc_binary` is a native (not Starlark-defined) rule, we can't
add `set_features` directly to it. For Starlark-defined rules, we could
omit `transition_rule` and just add the functionality directly.

`transition_rule` applies a [Starlark
transition](https://docs.bazel.build/versions/master/skylark/config.html#user-defined-transitions)
called `_copt_transition` that reads the value of `set_features` and sets
`//custom-settings:mycopts` accordingly.

Finally, this file declares a macro (also) called `cc_binary` that automates away all
this extra abstraction: the new macro `cc_binary` simply instantiates a
`transition_rule` with the desired `set_features` then passes all other
attributes directly to the native `cc_binary`. To the end user this makes it
look like `cc_binary` magically supports a new attribute.

* [BUILD](BUILD) - This defines three versions of a `cc_binary` all depending on
the same `cc_library`: one uses `feature`, the other uses `feature2`, and the
third fails to build because it "forgets" to set any feature.

* [main.cc](main.cc) and [lib.cc](lib.cc) for the actual C++ code.

To see it in action, cd to this directory and try the following commands:

```sh
$ bazel run :app_with_feature1
...
Running my app!
Building lib with feature 1!
```

```sh
$ bazel run :app_with_feature2
...
Running my app!
Building lib with feature 2!
```

```sh
$ bazel run :app_forgets_to_set_features
ERROR: $(MYWORKSPACE)/cc_binary_selectable_copts/BUILD:32:13: Configurable attribute "copts"
doesn't match this configuration: You must explicitly set which features you want!
```
This example relies on `select` and involves some duplication of values
(e.g. `"feature1"` is defined in `//custom_settings:mycopts` and `//:lib`
separately sets `"-Dfeature1"`). You could write more Starlark macros to make the
`BUILD` API even simpler. For example: not requiring any changes to
`cc_library` at all.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# We don't actually need an external repo to define Starlark settings. But
# Skylib provides convenience macros that reduce boilerplate, so we'll use that
# here.
#
# See https://docs.bazel.build/versions/master/skylark/config.html and
# https://github.com/bazelbuild/bazel-skylib) for more info.

load("@bazel_skylib//rules:common_settings.bzl", "string_flag")

# You can avoid the need for this entire file by using "--define" instead
# ("--define" is a native flag, built into Bazel). But we recommend this
# Starlark-based approach. This lets you model the setting you need much
# more precisely, offers typing and validation, mixes more cleanly with
# other flags, and doesn't risk namespace collisions on complicated projects
# (e.g. imgagine unrelated package reading "--define mode=1" for some
# overloaded concept of "mode").

string_flag(
# You can set this at the command line with "$ bazel build
# //starlark_configurations/cc_binary_selectable_copts:all
# --//starlark_configurations/cc_binary_selectable_copts/custom_settings:mycopts".
# It's also possible to disallow command line access (in this example
# we don't need that because cc_binary sets this implicitly without the
# end user even having to know this flag exists). See
# https://docs.bazel.build/versions/master/skylark/config.html#the-build_setting-rule-parameter
# for details.
name = "mycopts",
build_setting_default = "unset",
# We could also omit this attribute to allow any value:
values = [
"feature1",
"feature2",
# We'll use this to trigger an error if the binary forgets
# to set the feature it wants:
"unset",
],
)

# Predefine config_settings matching each feature. The "flag_values" attribute
# is used to match Starlark-defined flags.
#
# You could write more Starlark macros to further automate and simplify this.

config_setting(
name = "feature1",
flag_values = {":mycopts": "feature1"},
)

config_setting(
name = "feature2",
flag_values = {":mycopts": "feature2"},
)
91 changes: 91 additions & 0 deletions rules/starlark_configurations/cc_binary_selectable_copts/defs.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
def _copt_transition_impl(settings, attr):
_ignore = settings

# settings provides read access to existing flags. But
# this transition doesn't need to read any flags.
return {"//starlark_configurations/cc_binary_selectable_copts/custom_settings:mycopts": attr.set_features}

# This defines a Starlark transition and which flags it reads and
# writes. In this case we don't need to read any flags - we
# universally set --/custom_settings:mycopts according to whatever
# is set in the owning rule's "set_features" attribute.
_copt_transition = transition(
implementation = _copt_transition_impl,
inputs = [],
outputs = ["//starlark_configurations/cc_binary_selectable_copts/custom_settings:mycopts"],
)

# The implementation of transition_rule: all this does is copy the
# cc_binary's output to its own output and propagate its runfiles
# and executable to use for "$ bazel run".
#
# This makes transition_rule as close to a pure wrapper of cc_binary
# as possible.
def _transition_rule_impl(ctx):
actual_binary = ctx.attr.actual_binary[0]
outfile = ctx.actions.declare_file(ctx.label.name)
cc_binary_outfile = actual_binary[DefaultInfo].files.to_list()[0]

ctx.actions.run_shell(
inputs = [cc_binary_outfile],
outputs = [outfile],
command = "cp %s %s" % (cc_binary_outfile.path, outfile.path),
)
return [
DefaultInfo(
executable = outfile,
data_runfiles = actual_binary[DefaultInfo].data_runfiles,
),
]

# The purpose of this rule is to take a "set_features" attribute,
# invoke a transition that sets --//custom_settings:mycopts to the
# desired feature, then depend on a cc_binary whose deps will now
# be able to select() on that feature.
#
# You could add a transition_rule directly in your BUILD file. But for
# convenience we also define a cc_binary macro below so the BUILD can look
# as close to normal as possible.
transition_rule = rule(
implementation = _transition_rule_impl,
attrs = {
# This is where the user can set the feature they want.
"set_features": attr.string(default = "unset"),
# This is the cc_binary whose deps will select() on that feature.
# Note specificaly how it's configured with _copt_transition, which
# ensures that setting propagates down the graph.
"actual_binary": attr.label(cfg = _copt_transition),
# This is a stock Bazel requirement for any rule that uses Starlark
# transitions. It's okay to copy the below verbatim for all such rules.
#
# The purpose of this requirement is to give the ability to restrict
# which packages can invoke these rules, since Starlark transitions
# make much larger graphs possible that can have memory and performance
# consequences for your build. The whitelist defaults to "everything".
# But you can redefine it more strictly if you feel that's prudent.
"_whitelist_function_transition": attr.label(
default = "@bazel_tools//tools/whitelists/function_transition_whitelist",
),
},
# Making this executable means it works with "$ bazel run".
executable = True,
)

# Convenience macro: this instantiates a transition_rule with the given
# desired features, instantiates a cc_binary as a dependency of that rule,
# and fills out the cc_binary with all other parameters passed to this macro.
#
# The result is a wrapper over cc_binary that "magically" gives it a new
# feature-setting attribute. The only difference for a BUILD user is they need
# to load() this at the top of the BUILD file.
def cc_binary(name, set_features = None, **kwargs):
cc_binary_name = name + "_native_binary"
transition_rule(
name = name,
actual_binary = ":%s" % cc_binary_name,
set_features = set_features,
)
native.cc_binary(
name = cc_binary_name,
**kwargs
)
13 changes: 13 additions & 0 deletions rules/starlark_configurations/cc_binary_selectable_copts/lib.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <iostream>

void runlib() {
#ifdef feature1
std::cout << "Building lib with feature 1!\n";
#elif feature2
std::cout << "Building lib with feature 2!\n";
#else
std::cout << "Building lib without features. But the select() in the BUILD "
<< "file should prevent this case from building in the first "
<< "place.\n";
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <iostream>

void runlib(); // Defined in lib.cc

int main(int argc, char** argv) {
std::cout << "Running my app!\n";
runlib();
}
6 changes: 6 additions & 0 deletions rules/starlark_configurations/cc_test/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load(":defs.bzl", "test_arg_cc_test")

test_arg_cc_test(
name = "my-test",
srcs = ["mytest.cc"],
)
7 changes: 7 additions & 0 deletions rules/starlark_configurations/cc_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Example showing how to use [Starlark configuration](https://docs.bazel.build/versions/master/skylark/config.html) to write a
`cc_test` wrapper with a starlark transition

the `test_arg_cc_test` macro in `defs.bzl` defines a wrapper for basically a cc_test that has been transitioned.
This allows, e.g., the test itself to select attribute values based on the value of that transition. There is some
light magic in the `transition_rule` implementation that allows dependents of the `test_arg_cc_test` macros to
treat the targets it creates the exact same as a regular cc test.
Empty file.
46 changes: 46 additions & 0 deletions rules/starlark_configurations/cc_test/defs.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# We can transition on native options using this
# //command_line_option:<option-name> syntax
_BUILD_SETTING = "//command_line_option:test_arg"

def _test_arg_transition_impl(settings, attr):
_ignore = (settings, attr)

return {_BUILD_SETTING: ["new arg"]}

_test_arg_transition = transition(
implementation = _test_arg_transition_impl,
inputs = [],
outputs = [_BUILD_SETTING],
)

def _test_transition_rule_impl(ctx):
# We need to copy the executable because starlark doesn't allow
# providing an executable not created by the rule
executable_src = ctx.executable.actual_test
executable_dst = ctx.actions.declare_file(ctx.label.name)
ctx.actions.run_shell(
tools = [executable_src],
outputs = [executable_dst],
command = "cp %s %s" % (executable_src.path, executable_dst.path),
)
runfiles = ctx.attr.actual_test[0][DefaultInfo].default_runfiles
return [DefaultInfo(runfiles = runfiles, executable = executable_dst)]

transition_rule_test = rule(
implementation = _test_transition_rule_impl,
attrs = {
"actual_test": attr.label(cfg = _test_arg_transition, executable = True),
"_whitelist_function_transition": attr.label(
default = "@bazel_tools//tools/whitelists/function_transition_whitelist",
),
},
test = True,
)

def test_arg_cc_test(name, **kwargs):
cc_test_name = name + "_native_test"
transition_rule_test(
name = name,
actual_test = ":%s" % cc_test_name,
)
native.cc_test(name = cc_test_name, **kwargs)
5 changes: 5 additions & 0 deletions rules/starlark_configurations/cc_test/mytest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// a trivially passing test

int main() {
return 0;
}

0 comments on commit 2143d40

Please sign in to comment.