README.md
1# Writing a custom rule_based C++ toolchain with rule-based definition.
2
3Work in progress!
4
5This document serves two purposes:
6* Until complete, this serves as an agreement for the final user-facing API.
7* Once complete, this will serve as onboarding documentation.
8
9This section will be removed once complete.
10
11## Step 1: Define tools
12A tool is simply a binary. Just like any other bazel binary, a tool can specify
13additional files required to run.
14
15We can use any bazel binary as an input to anything that requires tools. In the
16example below, you could use both clang and ld as tools.
17
18```
19# @sysroot//:BUILD
20cc_tool(
21 name = "clang",
22 exe = ":bin/clang",
23 execution_requirements = ["requires-mem:24g"],
24 data = [...],
25)
26
27sh_binary(
28 name = "ld",
29 srcs = ["ld_wrapper.sh"],
30 data = [":bin/ld"],
31)
32
33```
34
35## Step 2: Generate action configs from those tools
36An action config is a mapping from action to:
37
38* A list of tools, (the first one matching the execution requirements is used).
39* A list of args and features that are always enabled for the action
40* A set of additional files required for the action
41
42Each action can only be specified once in the toolchain. Specifying multiple
43actions in a single `cc_action_type_config` is just a shorthand for specifying the
44same config for every one of those actions.
45
46If you're already familiar with how to define toolchains, the additional files
47is a replacement for `compile_files`, `link_files`, etc.
48
49Additionally, to replace `all_files`, we add `cc_additional_files_for_actions`.
50This allows you to specify that particular files are required for particular
51actions.
52
53We provide `additional_files` on the `cc_action_type_config` as a shorthand for
54specifying `cc_additional_files_for_actions`
55
56Warning: Implying a feature that is not listed directly in the toolchain will throw
57an error. This is to ensure you don't accidentally add a feature to the
58toolchain.
59
60```
61cc_action_type_config(
62 name = "c_compile",
63 actions = ["@rules_cc//actions:all_c_compile"],
64 tools = ["@sysroot//:clang"],
65 args = [":my_args"],
66 implies = [":my_feature"],
67 additional_files = ["@sysroot//:all_header_files"],
68)
69
70cc_additional_files_for_actions(
71 name = "all_action_files",
72 actions = ["@rules_cc//actions:all_actions"],
73 additional_files = ["@sysroot//:always_needed_files"]
74)
75```
76
77## Step 3: Define some arguments
78Arguments are our replacement for `flag_set` and `env_set`. To add arguments to
79our tools, we take heavy inspiration from bazel's
80[`Args`](https://bazel.build/rules/lib/builtins/Args) type. We provide the same
81API, with the following caveats:
82* `actions` specifies which actions the arguments apply to (same as `flag_set`).
83* `requires_any_of` is equivalent to `with_features` on the `flag_set`.
84* `args` may be used instead of `add` if your command-line is only strings.
85* `env` may be used to add environment variables to the arguments. Environment
86 variables set by later args take priority.
87* By default, all inputs are automatically added to the corresponding actions.
88 `additional_files` specifies files that are required for an action when using
89 that argument.
90
91```
92cc_args(
93 name = "inline",
94 actions = ["@rules_cc//actions:all_cpp_compile_actions"],
95 args = ["--foo"],
96 requires_any_of = [":feature"]
97 env = {"FOO": "bar"},
98 additional_files = [":file"],
99)
100```
101
102For more complex use cases, we use the same API as `Args`. Values are either:
103* A list of files (or a single file for `cc_add_args`).
104* Something returning `CcVariableInfo`, which is equivalent to a list of strings.
105
106```
107cc_variable(
108 name = "bar_baz",
109 values = ["bar", "baz"],
110)
111
112# Expands to CcVariableInfo(values = ["x86_64-unknown-linux-gnu"])
113custom_variable_rule(
114 name = "triple",
115 ...
116)
117
118# Taken from https://bazel.build/rules/lib/builtins/Args#add
119cc_add_args(
120 name = "single",
121 arg_name = "--platform",
122 value = ":triple", # Either a single file or a cc_variable
123 format = "%s",
124)
125
126# Taken from https://bazel.build/rules/lib/builtins/Args#add_all
127cc_add_args_all(
128 name = "multiple",
129 arg_name = "--foo",
130 values = [":file", ":file_set"], # Either files or cc_variable.
131 # map_each not supported. Write a custom rule if you want that.
132 format_each = "%s",
133 before_each = "--foo",
134 omit_if_empty = True,
135 uniquify = False,
136 # Expand_directories not yet supported.
137 terminate_with = "foo",
138)
139
140# Taken from https://bazel.build/rules/lib/builtins/Args#add_joined
141cc_add_args_joined(
142 name = "joined",
143 arg_name = "--foo",
144 values = [":file", ":file_set"], # Either files or cc_variable.
145 join_with = ",",
146 # map_each not supported. Write a custom rule if you want that.
147 format_each = "%s",
148 format_joined = "--foo=%s",
149 omit_if_empty = True,
150 uniquify = False,
151 # Expand_directories not yet supported.
152)
153
154cc_args(
155 name = "complex",
156 actions = ["@rules_cc//actions:c_compile"],
157 add = [":single", ":multiple", ":joined"],
158)
159
160cc_args_list(
161 name = "all_flags",
162 args = [":inline", ":complex"],
163)
164```
165
166## Step 4: Define some features
167A feature is a set of args and configurations that can be enabled or disabled.
168
169Although the existing toolchain recommends using features to avoid duplication
170of definitions, we recommend avoiding using features unless you want the user to
171be able to enable / disable the feature themselves. This is because we provide
172alternatives such as `cc_args_list` to allow combining arguments and
173specifying them on each action in the action config.
174
175```
176cc_feature(
177 name = "my_feature",
178 feature_name = "my_feature",
179 args = [":all_args"],
180 implies = [":other_feature"],
181)
182```
183
184## Step 5: Generate the toolchain
185The `cc_toolchain` macro:
186
187* Performs validation on the inputs (eg. no two action configs for a single
188 action)
189* Converts the type-safe providers to the unsafe ones in
190 `cc_toolchain_config_lib.bzl`
191* Generates a set of providers for each of the filegroups respectively
192* Generates the appropriate `native.cc_toolchain` invocation.
193
194```
195cc_toolchain(
196 name = "toolchain",
197 features = [":my_feature"]
198 unconditional_args = [":all_warnings"],
199 action_type_configs = [":c_compile"],
200 additional_files = [":all_action_files"],
201)
202```
203
204# Ancillary components for type-safe toolchains.
205## Well-known features
206Well-known features will be defined in `@rules_cc//features/well_known:*`.
207Any feature with `feature_name` in the well known features will have to specify
208overrides.
209
210`cc_toolchain` is aware of the builtin / well-known features. In order to
211ensure that a user understands that this overrides the builtin opt feature (I
212originally thought that it added extra flags to opt, but you still got the
213default ones, so that can definitely happen), and to ensure that they don't
214accidentally do so, we will force them to explicitly specify that it overrides
215the builtin one. This is essentially just an acknowledgement of "I know what
216I'm doing".
217
218Warning: Specifying two features with the same name is an error, unless one
219overrides the other.
220
221```
222cc_feature(
223 name = "opt",
224 ...,
225 overrides = "@rules_cc//features/well_known:opt",
226)
227```
228
229In addition to well-known features, we could also consider in future iterations
230to also use known features for partial migrations, where you still imply a
231feature that's still defined by the legacy API:
232
233```
234# Implementation
235def cc_legacy_features(name, features):
236 for feature in features:
237 cc_known_feature(name = name + "_" + feature.name)
238 cc_legacy_features(name = name, features = FEATURES)
239
240
241# Build file
242FOO = feature(name = "foo", args=[arg_group(...)])
243FEATURES = [FOO]
244cc_legacy_features(name = "legacy_features", features = FEATURES)
245
246cc_feature(name = "bar", implies = [":legacy_features_foo"])
247
248cc_toolchain(
249 name = "toolchain",
250 legacy_features = ":legacy_features",
251 features = [":bar"],
252)
253```
254
255## Mutual exclusion
256Features can be mutually exclusive.
257
258We allow two approaches to mutual exclusion - via features or via categories.
259
260The existing toolchain uses `provides` for both of these. We rename it so that
261it makes more sense semantically.
262
263```
264cc_feature(
265 name = "incompatible_with_my_feature",
266 feature_name = "bar",
267 mutually_exclusive = [":my_feature"],
268)
269
270
271# This is an example of how we would define compilation mode.
272# Since it already exists, this wouldn't work.
273cc_mutual_exclusion_category(
274 name = "compilation_mode",
275)
276
277cc_feature(
278 name = "opt",
279 ...
280 mutually_exclusive = [":compilation_mode"],
281)
282cc_feature(
283 name = "dbg",
284 ...
285 mutually_exclusive = [":compilation_mode"],
286)
287```
288
289## Feature requirements
290Feature requirements can come in two formats.
291
292For example:
293
294* Features can require some subset of features to be enabled.
295* Arguments can require some subset of features to be enabled, but others to be
296 disabled.
297
298This is very confusing for toolchain authors, so we will simplify things with
299the use of providers:
300
301* `cc_feature` will provide `feature`, `feature_set`, and `with_feature`
302* `cc_feature_set` will provide `feature_set` and `with_feature`.
303* `cc_feature_constraint` will provide `with_features` only.
304
305We will rename all `with_features` and `requires` to `requires_any_of`, to make
306it very clear that only one of the requirements needs to be met.
307
308```
309cc_feature_set(
310 name = "my_feature_set",
311 all_of = [":my_feature"],
312)
313
314cc_feature_constraint(
315 name = "my_feature_constraint",
316 all_of = [":my_feature"],
317 none_of = [":my_other_feature"],
318)
319
320cc_args(
321 name = "foo",
322 # All of these provide with_feature.
323 requires_any_of = [":my_feature", ":my_feature_set", ":my_feature_constraint"]
324)
325
326# my_feature_constraint would be an error here.
327cc_feature(
328 name = "foo",
329 # Both of these provide feature_set.
330 requires_any_of = [":my_feature", ":my_feature_set"]
331 implies = [":my_other_feature", :my_other_feature_set"],
332)
333```
334