1"""Rules for Cargo build scripts (`build.rs` files)""" 2 3load("@bazel_skylib//lib:paths.bzl", "paths") 4load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 5load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "CPP_COMPILE_ACTION_NAME", "C_COMPILE_ACTION_NAME") 6load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") 7load("//rust:defs.bzl", "rust_common") 8load("//rust:rust_common.bzl", "BuildInfo", "DepInfo") 9 10# buildifier: disable=bzl-visibility 11load( 12 "//rust/private:rustc.bzl", 13 "get_compilation_mode_opts", 14 "get_linker_and_args", 15) 16 17# buildifier: disable=bzl-visibility 18load( 19 "//rust/private:utils.bzl", 20 "dedent", 21 "expand_dict_value_locations", 22 "find_cc_toolchain", 23 "find_toolchain", 24 _name_to_crate_name = "name_to_crate_name", 25) 26 27# Reexport for cargo_build_script_wrapper.bzl 28name_to_crate_name = _name_to_crate_name 29 30def get_cc_compile_args_and_env(cc_toolchain, feature_configuration): 31 """Gather cc environment variables from the given `cc_toolchain` 32 33 Args: 34 cc_toolchain (cc_toolchain): The current rule's `cc_toolchain`. 35 feature_configuration (FeatureConfiguration): Class used to construct command lines from CROSSTOOL features. 36 37 Returns: 38 tuple: A tuple of the following items: 39 - (sequence): A flattened C command line flags for given action. 40 - (sequence): A flattened CXX command line flags for given action. 41 - (dict): C environment variables to be set for given action. 42 """ 43 compile_variables = cc_common.create_compile_variables( 44 feature_configuration = feature_configuration, 45 cc_toolchain = cc_toolchain, 46 ) 47 cc_c_args = cc_common.get_memory_inefficient_command_line( 48 feature_configuration = feature_configuration, 49 action_name = C_COMPILE_ACTION_NAME, 50 variables = compile_variables, 51 ) 52 cc_cxx_args = cc_common.get_memory_inefficient_command_line( 53 feature_configuration = feature_configuration, 54 action_name = CPP_COMPILE_ACTION_NAME, 55 variables = compile_variables, 56 ) 57 cc_env = cc_common.get_environment_variables( 58 feature_configuration = feature_configuration, 59 action_name = C_COMPILE_ACTION_NAME, 60 variables = compile_variables, 61 ) 62 return cc_c_args, cc_cxx_args, cc_env 63 64def _pwd_flags(args): 65 """Prefix execroot-relative paths of known arguments with ${pwd}. 66 67 Args: 68 args (list): List of tool arguments. 69 70 Returns: 71 list: The modified argument list. 72 """ 73 res = [] 74 for arg in args: 75 s, opt, path = arg.partition("--sysroot=") 76 if s == "" and not paths.is_absolute(path): 77 res.append("{}${{pwd}}/{}".format(opt, path)) 78 else: 79 res.append(arg) 80 return res 81 82def _feature_enabled(ctx, feature_name, default = False): 83 """Check if a feature is enabled. 84 85 If the feature is explicitly enabled or disabled, return accordingly. 86 87 In the case where the feature is not explicitly enabled or disabled, return the default value. 88 89 Args: 90 ctx: The context object. 91 feature_name: The name of the feature. 92 default: The default value to return if the feature is not explicitly enabled or disabled. 93 94 Returns: 95 Boolean defining whether the feature is enabled. 96 """ 97 if feature_name in ctx.disabled_features: 98 return False 99 100 if feature_name in ctx.features: 101 return True 102 103 return default 104 105def _cargo_build_script_impl(ctx): 106 """The implementation for the `cargo_build_script` rule. 107 108 Args: 109 ctx (ctx): The rules context object 110 111 Returns: 112 list: A list containing a BuildInfo provider 113 """ 114 script = ctx.executable.script 115 toolchain = find_toolchain(ctx) 116 out_dir = ctx.actions.declare_directory(ctx.label.name + ".out_dir") 117 env_out = ctx.actions.declare_file(ctx.label.name + ".env") 118 dep_env_out = ctx.actions.declare_file(ctx.label.name + ".depenv") 119 flags_out = ctx.actions.declare_file(ctx.label.name + ".flags") 120 link_flags = ctx.actions.declare_file(ctx.label.name + ".linkflags") 121 link_search_paths = ctx.actions.declare_file(ctx.label.name + ".linksearchpaths") # rustc-link-search, propagated from transitive dependencies 122 manifest_dir = "%s.runfiles/%s/%s" % (script.path, ctx.label.workspace_name or ctx.workspace_name, ctx.label.package) 123 compilation_mode_opt_level = get_compilation_mode_opts(ctx, toolchain).opt_level 124 125 streams = struct( 126 stdout = ctx.actions.declare_file(ctx.label.name + ".stdout.log"), 127 stderr = ctx.actions.declare_file(ctx.label.name + ".stderr.log"), 128 ) 129 130 pkg_name = name_to_pkg_name(ctx.label.name) 131 132 toolchain_tools = [toolchain.all_files] 133 134 cc_toolchain = find_cpp_toolchain(ctx) 135 136 # Start with the default shell env, which contains any --action_env 137 # settings passed in on the command line. 138 env = dict(ctx.configuration.default_shell_env) 139 140 env.update({ 141 "CARGO_CRATE_NAME": name_to_crate_name(pkg_name), 142 "CARGO_MANIFEST_DIR": manifest_dir, 143 "CARGO_PKG_NAME": pkg_name, 144 "HOST": toolchain.exec_triple.str, 145 "NUM_JOBS": "1", 146 "OPT_LEVEL": compilation_mode_opt_level, 147 "RUSTC": toolchain.rustc.path, 148 "TARGET": toolchain.target_flag_value, 149 # OUT_DIR is set by the runner itself, rather than on the action. 150 }) 151 152 # This isn't exactly right, but Bazel doesn't have exact views of "debug" and "release", so... 153 env.update({ 154 "DEBUG": {"dbg": "true", "fastbuild": "true", "opt": "false"}.get(ctx.var["COMPILATION_MODE"], "true"), 155 "PROFILE": {"dbg": "debug", "fastbuild": "debug", "opt": "release"}.get(ctx.var["COMPILATION_MODE"], "unknown"), 156 }) 157 158 if ctx.attr.version: 159 version = ctx.attr.version.split("+")[0].split(".") 160 patch = version[2].split("-") if len(version) > 2 else [""] 161 env["CARGO_PKG_VERSION_MAJOR"] = version[0] 162 env["CARGO_PKG_VERSION_MINOR"] = version[1] if len(version) > 1 else "" 163 env["CARGO_PKG_VERSION_PATCH"] = patch[0] 164 env["CARGO_PKG_VERSION_PRE"] = patch[1] if len(patch) > 1 else "" 165 env["CARGO_PKG_VERSION"] = ctx.attr.version 166 167 # Pull in env vars which may be required for the cc_toolchain to work (e.g. on OSX, the SDK version). 168 # We hope that the linker env is sufficient for the whole cc_toolchain. 169 cc_toolchain, feature_configuration = find_cc_toolchain(ctx) 170 linker, link_args, linker_env = get_linker_and_args(ctx, ctx.attr, "bin", cc_toolchain, feature_configuration, None) 171 env.update(**linker_env) 172 env["LD"] = linker 173 env["LDFLAGS"] = " ".join(_pwd_flags(link_args)) 174 175 # MSVC requires INCLUDE to be set 176 cc_c_args, cc_cxx_args, cc_env = get_cc_compile_args_and_env(cc_toolchain, feature_configuration) 177 include = cc_env.get("INCLUDE") 178 if include: 179 env["INCLUDE"] = include 180 181 if cc_toolchain: 182 toolchain_tools.append(cc_toolchain.all_files) 183 184 cc_executable = cc_toolchain.compiler_executable 185 if cc_executable: 186 env["CC"] = cc_executable 187 env["CXX"] = cc_executable 188 ar_executable = cc_toolchain.ar_executable 189 if ar_executable: 190 env["AR"] = ar_executable 191 192 # Populate CFLAGS and CXXFLAGS that cc-rs relies on when building from source, in particular 193 # to determine the deployment target when building for apple platforms (`macosx-version-min` 194 # for example, itself derived from the `macos_minimum_os` Bazel argument). 195 env["CFLAGS"] = " ".join(_pwd_flags(cc_c_args)) 196 env["CXXFLAGS"] = " ".join(_pwd_flags(cc_cxx_args)) 197 198 # Inform build scripts of rustc flags 199 # https://github.com/rust-lang/cargo/issues/9600 200 env["CARGO_ENCODED_RUSTFLAGS"] = "\\x1f".join([ 201 # Allow build scripts to locate the generated sysroot 202 "--sysroot=${{pwd}}/{}".format(toolchain.sysroot), 203 ] + ctx.attr.rustc_flags) 204 205 for f in ctx.attr.crate_features: 206 env["CARGO_FEATURE_" + f.upper().replace("-", "_")] = "1" 207 208 links = ctx.attr.links or "" 209 if links: 210 env["CARGO_MANIFEST_LINKS"] = links 211 212 # Add environment variables from the Rust toolchain. 213 env.update(toolchain.env) 214 215 # Gather data from the `toolchains` attribute. 216 for target in ctx.attr.toolchains: 217 if DefaultInfo in target: 218 toolchain_tools.extend([ 219 target[DefaultInfo].files, 220 target[DefaultInfo].default_runfiles.files, 221 ]) 222 if platform_common.ToolchainInfo in target: 223 all_files = getattr(target[platform_common.ToolchainInfo], "all_files", depset([])) 224 if type(all_files) == "list": 225 all_files = depset(all_files) 226 toolchain_tools.append(all_files) 227 if platform_common.TemplateVariableInfo in target: 228 variables = getattr(target[platform_common.TemplateVariableInfo], "variables", depset([])) 229 env.update(variables) 230 231 _merge_env_dict(env, expand_dict_value_locations( 232 ctx, 233 ctx.attr.build_script_env, 234 getattr(ctx.attr, "data", []) + 235 getattr(ctx.attr, "compile_data", []) + 236 getattr(ctx.attr, "tools", []), 237 )) 238 239 tools = depset( 240 direct = [ 241 script, 242 ctx.executable._cargo_build_script_runner, 243 ] + ctx.files.data + ctx.files.tools + ([toolchain.target_json] if toolchain.target_json else []), 244 transitive = toolchain_tools, 245 ) 246 247 # dep_env_file contains additional environment variables coming from 248 # direct dependency sys-crates' build scripts. These need to be made 249 # available to the current crate build script. 250 # See https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages 251 # for details. 252 args = ctx.actions.args() 253 args.add(script) 254 args.add(links) 255 args.add(out_dir.path) 256 args.add(env_out) 257 args.add(flags_out) 258 args.add(link_flags) 259 args.add(link_search_paths) 260 args.add(dep_env_out) 261 args.add(streams.stdout) 262 args.add(streams.stderr) 263 args.add(ctx.attr.rundir) 264 265 build_script_inputs = [] 266 for dep in ctx.attr.link_deps: 267 if rust_common.dep_info in dep and dep[rust_common.dep_info].dep_env: 268 dep_env_file = dep[rust_common.dep_info].dep_env 269 args.add(dep_env_file.path) 270 build_script_inputs.append(dep_env_file) 271 for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list(): 272 build_script_inputs.append(dep_build_info.out_dir) 273 274 for dep in ctx.attr.deps: 275 for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list(): 276 build_script_inputs.append(dep_build_info.out_dir) 277 278 experimental_symlink_execroot = ctx.attr._experimental_symlink_execroot[BuildSettingInfo].value or \ 279 _feature_enabled(ctx, "symlink-exec-root") 280 281 if experimental_symlink_execroot: 282 env["RULES_RUST_SYMLINK_EXEC_ROOT"] = "1" 283 284 ctx.actions.run( 285 executable = ctx.executable._cargo_build_script_runner, 286 arguments = [args], 287 outputs = [out_dir, env_out, flags_out, link_flags, link_search_paths, dep_env_out, streams.stdout, streams.stderr], 288 tools = tools, 289 inputs = build_script_inputs, 290 mnemonic = "CargoBuildScriptRun", 291 progress_message = "Running Cargo build script {}".format(pkg_name), 292 env = env, 293 toolchain = None, 294 ) 295 296 return [ 297 BuildInfo( 298 out_dir = out_dir, 299 rustc_env = env_out, 300 dep_env = dep_env_out, 301 flags = flags_out, 302 linker_flags = link_flags, 303 link_search_paths = link_search_paths, 304 compile_data = depset([]), 305 ), 306 OutputGroupInfo( 307 streams = depset([streams.stdout, streams.stderr]), 308 out_dir = depset([out_dir]), 309 ), 310 ] 311 312cargo_build_script = rule( 313 doc = ( 314 "A rule for running a crate's `build.rs` files to generate build information " + 315 "which is then used to determine how to compile said crate." 316 ), 317 implementation = _cargo_build_script_impl, 318 attrs = { 319 "build_script_env": attr.string_dict( 320 doc = "Environment variables for build scripts.", 321 ), 322 "crate_features": attr.string_list( 323 doc = "The list of rust features that the build script should consider activated.", 324 ), 325 "data": attr.label_list( 326 doc = "Data required by the build script.", 327 allow_files = True, 328 ), 329 "deps": attr.label_list( 330 doc = "The Rust build-dependencies of the crate", 331 providers = [rust_common.dep_info], 332 cfg = "exec", 333 ), 334 "link_deps": attr.label_list( 335 doc = dedent("""\ 336 The subset of the Rust (normal) dependencies of the crate that 337 have the links attribute and therefore provide environment 338 variables to this build script. 339 """), 340 providers = [rust_common.dep_info], 341 ), 342 "links": attr.string( 343 doc = "The name of the native library this crate links against.", 344 ), 345 "rundir": attr.string( 346 default = "", 347 doc = dedent("""\ 348 A directory to cd to before the cargo_build_script is run. This should be a path relative to the exec root. 349 350 The default behaviour (and the behaviour if rundir is set to the empty string) is to change to the relative path corresponding to the cargo manifest directory, which replicates the normal behaviour of cargo so it is easy to write compatible build scripts. 351 352 If set to `.`, the cargo build script will run in the exec root. 353 """), 354 ), 355 "rustc_flags": attr.string_list( 356 doc = dedent("""\ 357 List of compiler flags passed to `rustc`. 358 359 These strings are subject to Make variable expansion for predefined 360 source/output path variables like `$location`, `$execpath`, and 361 `$rootpath`. This expansion is useful if you wish to pass a generated 362 file of arguments to rustc: `@$(location //package:target)`. 363 """), 364 ), 365 # The source of truth will be the `cargo_build_script` macro until stardoc 366 # implements documentation inheritence. See https://github.com/bazelbuild/stardoc/issues/27 367 "script": attr.label( 368 doc = "The binary script to run, generally a `rust_binary` target.", 369 executable = True, 370 allow_files = True, 371 mandatory = True, 372 cfg = "exec", 373 ), 374 "tools": attr.label_list( 375 doc = "Tools required by the build script.", 376 allow_files = True, 377 cfg = "exec", 378 ), 379 "version": attr.string( 380 doc = "The semantic version (semver) of the crate", 381 ), 382 "_cargo_build_script_runner": attr.label( 383 executable = True, 384 allow_files = True, 385 default = Label("//cargo/cargo_build_script_runner:cargo_build_script_runner"), 386 cfg = "exec", 387 ), 388 "_cc_toolchain": attr.label( 389 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), 390 ), 391 "_experimental_symlink_execroot": attr.label( 392 default = Label("//cargo/settings:experimental_symlink_execroot"), 393 ), 394 }, 395 fragments = ["cpp"], 396 toolchains = [ 397 str(Label("//rust:toolchain_type")), 398 "@bazel_tools//tools/cpp:toolchain_type", 399 ], 400) 401 402def _merge_env_dict(prefix_dict, suffix_dict): 403 """Merges suffix_dict into prefix_dict, appending rather than replacing certain env vars.""" 404 for key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]: 405 if key in prefix_dict and key in suffix_dict and prefix_dict[key]: 406 prefix_dict[key] += " " + suffix_dict.pop(key) 407 prefix_dict.update(suffix_dict) 408 409def name_to_pkg_name(name): 410 """Sanitize the name of cargo_build_script targets. 411 412 Args: 413 name (str): The name value pass to the `cargo_build_script` wrapper. 414 415 Returns: 416 str: A cleaned up name for a build script target. 417 """ 418 if name.endswith("_bs"): 419 return name[:-len("_bs")] 420 return name 421 422def _cargo_dep_env_implementation(ctx): 423 empty_file = ctx.actions.declare_file(ctx.label.name + ".empty_file") 424 empty_dir = ctx.actions.declare_directory(ctx.label.name + ".empty_dir") 425 ctx.actions.write( 426 output = empty_file, 427 content = "", 428 ) 429 ctx.actions.run( 430 outputs = [empty_dir], 431 executable = "true", 432 ) 433 434 build_infos = [] 435 out_dir = ctx.file.out_dir 436 if out_dir: 437 if not out_dir.is_directory: 438 fail("out_dir must be a directory artifact") 439 440 # BuildInfos in this list are collected up for all transitive cargo_build_script 441 # dependencies. This is important for any flags set in `dep_env` which reference this 442 # `out_dir`. 443 # 444 # TLDR: This BuildInfo propagates up build script dependencies. 445 build_infos.append(BuildInfo( 446 dep_env = empty_file, 447 flags = empty_file, 448 linker_flags = empty_file, 449 link_search_paths = empty_file, 450 out_dir = out_dir, 451 rustc_env = empty_file, 452 compile_data = depset([]), 453 )) 454 return [ 455 DefaultInfo(files = depset(ctx.files.src)), 456 # Parts of this BuildInfo is used when building all transitive dependencies 457 # (cargo_build_script and otherwise), alongside the DepInfo. This is how other rules 458 # identify this one as a valid dependency, but we don't otherwise have a use for it. 459 # 460 # TLDR: This BuildInfo propagates up normal (non build script) depenencies. 461 # 462 # In the future, we could consider setting rustc_env here, and also propagating dep_dir 463 # so files in it can be referenced there. 464 BuildInfo( 465 dep_env = empty_file, 466 flags = empty_file, 467 linker_flags = empty_file, 468 link_search_paths = empty_file, 469 out_dir = None, 470 rustc_env = empty_file, 471 compile_data = depset([]), 472 ), 473 # Information here is used directly by dependencies, and it is an error to have more than 474 # one dependency which sets this. This is the main way to specify information from build 475 # scripts, which is what we're looking to do. 476 DepInfo( 477 dep_env = ctx.file.src, 478 direct_crates = depset(), 479 link_search_path_files = depset(), 480 transitive_build_infos = depset(direct = build_infos), 481 transitive_crate_outputs = depset(), 482 transitive_crates = depset(), 483 transitive_noncrates = depset(), 484 ), 485 ] 486 487cargo_dep_env = rule( 488 implementation = _cargo_dep_env_implementation, 489 doc = ( 490 "A rule for generating variables for dependent `cargo_build_script`s " + 491 "without a build script. This is useful for using Bazel rules instead " + 492 "of a build script, while also generating configuration information " + 493 "for build scripts which depend on this crate." 494 ), 495 attrs = { 496 "out_dir": attr.label( 497 doc = dedent("""\ 498 Folder containing additional inputs when building all direct dependencies. 499 500 This has the same effect as a `cargo_build_script` which prints 501 puts files into `$OUT_DIR`, but without requiring a build script. 502 """), 503 allow_single_file = True, 504 mandatory = False, 505 ), 506 "src": attr.label( 507 doc = dedent("""\ 508 File containing additional environment variables to set for build scripts of direct dependencies. 509 510 This has the same effect as a `cargo_build_script` which prints 511 `cargo:VAR=VALUE` lines, but without requiring a build script. 512 513 This files should contain a single variable per line, of format 514 `NAME=value`, and newlines may be included in a value by ending a 515 line with a trailing back-slash (`\\\\`). 516 """), 517 allow_single_file = True, 518 mandatory = True, 519 ), 520 }, 521) 522