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