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