1"""Rules for vendoring Bazel targets into existing workspaces""" 2 3load("//crate_universe/private:generate_utils.bzl", "compile_config", "render_config") 4load("//crate_universe/private:splicing_utils.bzl", "kebab_case_keys", generate_splicing_config = "splicing_config") 5load("//crate_universe/private:urls.bzl", "CARGO_BAZEL_LABEL") 6load("//rust/platform:triple_mappings.bzl", "SUPPORTED_PLATFORM_TRIPLES") 7 8_UNIX_WRAPPER = """\ 9#!/usr/bin/env bash 10set -euo pipefail 11export RUNTIME_PWD="$(pwd)" 12if [[ -z "${{BAZEL_REAL:-}}" ]]; then 13 BAZEL_REAL="$(which bazel || echo 'bazel')" 14fi 15 16# The path needs to be preserved to prevent bazel from starting with different 17# startup options (requiring a restart of bazel). 18# If you provide an empty path, bazel starts itself with 19# --default_system_javabase set to the empty string, but if you provide a path, 20# it may set it to a value (eg. "/usr/local/buildtools/java/jdk11"). 21exec env - BAZEL_REAL="${{BAZEL_REAL}}" BUILD_WORKSPACE_DIRECTORY="${{BUILD_WORKSPACE_DIRECTORY}}" PATH="${{PATH}}" {env} \\ 22"{bin}" {args} "$@" 23""" 24 25_WINDOWS_WRAPPER = """\ 26@ECHO OFF 27set RUNTIME_PWD=%CD% 28{env} 29 30call {bin} {args} %@% 31""" 32 33CARGO_BAZEL_GENERATOR_PATH = "CARGO_BAZEL_GENERATOR_PATH" 34 35def _default_render_config(): 36 return json.decode(render_config()) 37 38def _runfiles_path(file, is_windows): 39 if is_windows: 40 runtime_pwd_var = "%RUNTIME_PWD%" 41 else: 42 runtime_pwd_var = "${RUNTIME_PWD}" 43 44 return "{}/{}".format(runtime_pwd_var, file.short_path) 45 46def _is_windows(ctx): 47 toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")] 48 return toolchain.target_os == "windows" 49 50def _get_output_package(ctx): 51 # Determine output directory 52 if ctx.attr.vendor_path.startswith("/"): 53 output = ctx.attr.vendor_path 54 else: 55 output = "{}/{}".format( 56 ctx.label.package, 57 ctx.attr.vendor_path, 58 ) 59 return output.lstrip("/") 60 61def _write_data_file(ctx, name, data): 62 file = ctx.actions.declare_file("{}.{}".format(ctx.label.name, name)) 63 ctx.actions.write( 64 output = file, 65 content = data, 66 ) 67 return file 68 69def _prepare_manifest_path(target): 70 """Generate manifest paths that are resolvable by `cargo_bazel::SplicingManifest::resolve` 71 72 Args: 73 target (Target): A `crate_vendor.manifest` target 74 75 Returns: 76 str: A string representing the path to a manifest. 77 """ 78 files = target[DefaultInfo].files.to_list() 79 if len(files) != 1: 80 fail("The manifest {} hand an unexpected number of files: {}".format( 81 target.label, 82 files, 83 )) 84 85 manifest = files[0] 86 87 if target.label.workspace_root.startswith("external"): 88 # The short path of an external file is expected to start with `../` 89 if not manifest.short_path.startswith("../"): 90 fail("Unexpected shortpath for {}: {}".format( 91 manifest, 92 manifest.short_path, 93 )) 94 return manifest.short_path.replace("../", "${output_base}/external/", 1) 95 96 return "${build_workspace_directory}/" + manifest.short_path 97 98def _write_splicing_manifest(ctx): 99 # Manifests are required to be single files 100 manifests = {_prepare_manifest_path(m): str(m.label) for m in ctx.attr.manifests} 101 102 manifest = _write_data_file( 103 ctx = ctx, 104 name = "cargo-bazel-splicing-manifest.json", 105 data = generate_splicing_manifest( 106 packages = ctx.attr.packages, 107 splicing_config = ctx.attr.splicing_config, 108 cargo_config = ctx.attr.cargo_config, 109 manifests = manifests, 110 manifest_to_path = _prepare_manifest_path, 111 ), 112 ) 113 114 is_windows = _is_windows(ctx) 115 116 args = ["--splicing-manifest", _runfiles_path(manifest, is_windows)] 117 runfiles = [manifest] + ctx.files.manifests + ([ctx.file.cargo_config] if ctx.attr.cargo_config else []) 118 return args, runfiles 119 120def generate_splicing_manifest(packages, splicing_config, cargo_config, manifests, manifest_to_path): 121 # Deserialize information about direct packages 122 direct_packages_info = { 123 # Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects. 124 pkg: kebab_case_keys(dict(json.decode(data))) 125 for (pkg, data) in packages.items() 126 } 127 128 config = json.decode(splicing_config or generate_splicing_config()) 129 splicing_manifest_content = { 130 "cargo_config": str(manifest_to_path(cargo_config)) if cargo_config else None, 131 "direct_packages": direct_packages_info, 132 "manifests": manifests, 133 } 134 135 return json.encode_indent( 136 dict(dict(config).items() + splicing_manifest_content.items()), 137 indent = " " * 4, 138 ) 139 140def _write_config_file(ctx): 141 workspace_name = ctx.workspace_name 142 if workspace_name == "__main__" or ctx.workspace_name == "_main": 143 workspace_name = "" 144 145 config = _write_data_file( 146 ctx = ctx, 147 name = "cargo-bazel-config.json", 148 data = generate_config_file( 149 ctx, 150 mode = ctx.attr.mode, 151 annotations = ctx.attr.annotations, 152 generate_binaries = ctx.attr.generate_binaries, 153 generate_build_scripts = ctx.attr.generate_build_scripts, 154 generate_target_compatible_with = ctx.attr.generate_target_compatible_with, 155 supported_platform_triples = ctx.attr.supported_platform_triples, 156 repository_name = ctx.attr.repository_name, 157 output_pkg = _get_output_package(ctx), 158 workspace_name = workspace_name, 159 render_config = dict(json.decode(ctx.attr.render_config)) if ctx.attr.render_config else None, 160 ), 161 ) 162 163 is_windows = _is_windows(ctx) 164 args = ["--config", _runfiles_path(config, is_windows)] 165 runfiles = [config] + ctx.files.manifests 166 return args, runfiles 167 168def generate_config_file( 169 ctx, 170 mode, 171 annotations, 172 generate_binaries, 173 generate_build_scripts, 174 generate_target_compatible_with, 175 supported_platform_triples, 176 repository_name, 177 output_pkg, 178 workspace_name, 179 render_config, 180 repository_ctx = None): 181 """Writes the rendering config to cargo-bazel-config.json. 182 183 Args: 184 ctx: The rule's context. 185 mode (str): The vendoring mode. 186 annotations: Any annotations provided. 187 generate_binaries (bool): Whether to generate binaries for the crates. 188 generate_build_scripts (bool): Whether to generate BUILD.bazel files. 189 generate_target_compatible_with (bool): DEPRECATED: Moved to `render_config`. 190 supported_platform_triples (str): The platform triples to support in 191 the generated BUILD.bazel files. 192 repository_name (str): The name of the repository to generate. 193 output_pkg: The path to the package containing the build files. 194 workspace_name (str): The name of the workspace. 195 render_config: The render config to use. 196 repository_ctx (repository_ctx, optional): A repository context object 197 used for enabling certain functionality. 198 199 Returns: 200 file: The cargo-bazel-config.json written. 201 """ 202 default_render_config = _default_render_config() 203 if render_config == None: 204 render_config = default_render_config 205 206 if mode == "local": 207 build_file_base_template = "//{}/{{name}}-{{version}}:BUILD.bazel".format(output_pkg) 208 if workspace_name != "": 209 build_file_base_template = "@{}//{}/{{name}}-{{version}}:BUILD.bazel".format(workspace_name, output_pkg) 210 crate_label_template = "//{}/{{name}}-{{version}}:{{target}}".format( 211 output_pkg, 212 ) 213 else: 214 build_file_base_template = "//{}:BUILD.{{name}}-{{version}}.bazel".format(output_pkg) 215 if workspace_name != "": 216 build_file_base_template = "@{}//{}:BUILD.{{name}}-{{version}}.bazel".format(workspace_name, output_pkg) 217 crate_label_template = render_config["crate_label_template"] 218 219 # If `workspace_name` is blank (such as when using modules), the `@{}//{}:{{file}}` template would generate 220 # a reference like `Label(@//<stuff>)`. This causes issues if the module doing the `crates_vendor`ing is not the root module. 221 # See: https://github.com/bazelbuild/rules_rust/issues/2661 222 crates_module_template_value = "//{}:{{file}}".format(output_pkg) 223 if workspace_name != "": 224 crates_module_template_value = "@{}//{}:{{file}}".format( 225 workspace_name, 226 output_pkg, 227 ) 228 229 updates = { 230 "build_file_template": build_file_base_template, 231 "crate_label_template": crate_label_template, 232 "crates_module_template": crates_module_template_value, 233 "vendor_mode": mode, 234 } 235 236 # "crate_label_template" is explicitly supported above in non-local modes 237 excluded_from_key_check = ["crate_label_template"] 238 239 for key in updates: 240 if (render_config[key] != default_render_config[key]) and key not in excluded_from_key_check: 241 fail("The `crates_vendor.render_config` attribute does not support the `{}` parameter. Please update {} to remove this value.".format( 242 key, 243 ctx.label, 244 )) 245 246 render_config.update(updates) 247 248 # Allow users to override the regen command. 249 if "regen_command" not in render_config or not render_config["regen_command"]: 250 render_config.update({"regen_command": "bazel run {}".format(ctx.label)}) 251 252 config_data = compile_config( 253 crate_annotations = annotations, 254 generate_binaries = generate_binaries, 255 generate_build_scripts = generate_build_scripts, 256 generate_target_compatible_with = generate_target_compatible_with, 257 cargo_config = None, 258 render_config = render_config, 259 supported_platform_triples = supported_platform_triples, 260 repository_name = repository_name or ctx.label.name, 261 repository_ctx = repository_ctx, 262 ) 263 264 return json.encode_indent( 265 config_data, 266 indent = " " * 4, 267 ) 268 269def _crates_vendor_impl(ctx): 270 toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")] 271 is_windows = _is_windows(ctx) 272 273 environ = { 274 "CARGO": _runfiles_path(toolchain.cargo, is_windows), 275 "RUSTC": _runfiles_path(toolchain.rustc, is_windows), 276 } 277 278 args = ["vendor"] 279 280 cargo_bazel_runfiles = [] 281 282 # Allow action envs to override the use of the cargo-bazel target. 283 if CARGO_BAZEL_GENERATOR_PATH in ctx.var: 284 bin_path = ctx.var[CARGO_BAZEL_GENERATOR_PATH] 285 elif ctx.executable.cargo_bazel: 286 bin_path = _runfiles_path(ctx.executable.cargo_bazel, is_windows) 287 cargo_bazel_runfiles.append(ctx.executable.cargo_bazel) 288 else: 289 fail("{} is missing either the `cargo_bazel` attribute or the '{}' action env".format( 290 ctx.label, 291 CARGO_BAZEL_GENERATOR_PATH, 292 )) 293 294 # Generate config file 295 config_args, config_runfiles = _write_config_file(ctx) 296 args.extend(config_args) 297 cargo_bazel_runfiles.extend(config_runfiles) 298 299 # Generate splicing manifest 300 splicing_manifest_args, splicing_manifest_runfiles = _write_splicing_manifest(ctx) 301 args.extend(splicing_manifest_args) 302 cargo_bazel_runfiles.extend(splicing_manifest_runfiles) 303 304 # Add an optional `Cargo.lock` file. 305 if ctx.attr.cargo_lockfile: 306 args.extend([ 307 "--cargo-lockfile", 308 _runfiles_path(ctx.file.cargo_lockfile, is_windows), 309 ]) 310 cargo_bazel_runfiles.extend([ctx.file.cargo_lockfile]) 311 312 # Optionally include buildifier 313 if ctx.attr.buildifier: 314 args.extend(["--buildifier", _runfiles_path(ctx.executable.buildifier, is_windows)]) 315 cargo_bazel_runfiles.append(ctx.executable.buildifier) 316 317 # Optionally include an explicit `bazel` path 318 if ctx.attr.bazel: 319 args.extend(["--bazel", _runfiles_path(ctx.executable.bazel, is_windows)]) 320 cargo_bazel_runfiles.append(ctx.executable.bazel) 321 322 # Determine platform specific settings 323 if is_windows: 324 extension = ".bat" 325 template = _WINDOWS_WRAPPER 326 env_template = "\nset {}={}" 327 else: 328 extension = ".sh" 329 template = _UNIX_WRAPPER 330 env_template = "{}={}" 331 332 # Write the wrapper script 333 runner = ctx.actions.declare_file(ctx.label.name + extension) 334 ctx.actions.write( 335 output = runner, 336 content = template.format( 337 env = " ".join([env_template.format(key, val) for key, val in environ.items()]), 338 bin = bin_path, 339 args = " ".join(args), 340 ), 341 is_executable = True, 342 ) 343 344 return DefaultInfo( 345 files = depset([runner]), 346 runfiles = ctx.runfiles( 347 files = cargo_bazel_runfiles, 348 transitive_files = toolchain.all_files, 349 ), 350 executable = runner, 351 ) 352 353CRATES_VENDOR_ATTRS = { 354 "annotations": attr.string_list_dict( 355 doc = "Extra settings to apply to crates. See [crate.annotation](#crateannotation).", 356 ), 357 "bazel": attr.label( 358 doc = "The path to a bazel binary used to locate the output_base for the current workspace.", 359 cfg = "exec", 360 executable = True, 361 allow_files = True, 362 ), 363 "buildifier": attr.label( 364 doc = "The path to a [buildifier](https://github.com/bazelbuild/buildtools/blob/5.0.1/buildifier/README.md) binary used to format generated BUILD files.", 365 cfg = "exec", 366 executable = True, 367 allow_files = True, 368 default = Label("//crate_universe/private/vendor:buildifier"), 369 ), 370 "cargo_bazel": attr.label( 371 doc = ( 372 "The cargo-bazel binary to use for vendoring. If this attribute is not set, then a " + 373 "`{}` action env will be used.".format(CARGO_BAZEL_GENERATOR_PATH) 374 ), 375 cfg = "exec", 376 executable = True, 377 allow_files = True, 378 default = CARGO_BAZEL_LABEL, 379 ), 380 "cargo_config": attr.label( 381 doc = "A [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) file.", 382 allow_single_file = True, 383 ), 384 "cargo_lockfile": attr.label( 385 doc = "The path to an existing `Cargo.lock` file", 386 allow_single_file = True, 387 ), 388 "generate_binaries": attr.bool( 389 doc = ( 390 "Whether to generate `rust_binary` targets for all the binary crates in every package. " + 391 "By default only the `rust_library` targets are generated." 392 ), 393 default = False, 394 ), 395 "generate_build_scripts": attr.bool( 396 doc = ( 397 "Whether or not to generate " + 398 "[cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) by default." 399 ), 400 default = True, 401 ), 402 "generate_target_compatible_with": attr.bool( 403 doc = "DEPRECATED: Moved to `render_config`.", 404 default = True, 405 ), 406 "manifests": attr.label_list( 407 doc = "A list of Cargo manifests (`Cargo.toml` files).", 408 allow_files = ["Cargo.toml"], 409 ), 410 "mode": attr.string( 411 doc = ( 412 "Flags determining how crates should be vendored. `local` is where crate source and BUILD files are " + 413 "written to the repository. `remote` is where only BUILD files are written and repository rules " + 414 "used to fetch source code." 415 ), 416 values = [ 417 "local", 418 "remote", 419 ], 420 default = "remote", 421 ), 422 "packages": attr.string_dict( 423 doc = "A set of crates (packages) specifications to depend on. See [crate.spec](#crate.spec).", 424 ), 425 "render_config": attr.string( 426 doc = ( 427 "The configuration flags to use for rendering. Use `//crate_universe:defs.bzl\\%render_config` to " + 428 "generate the value for this field. If unset, the defaults defined there will be used." 429 ), 430 ), 431 "repository_name": attr.string( 432 doc = "The name of the repository to generate for `remote` vendor modes. If unset, the label name will be used", 433 ), 434 "splicing_config": attr.string( 435 doc = ( 436 "The configuration flags to use for splicing Cargo maniests. Use `//crate_universe:defs.bzl\\%rsplicing_config` to " + 437 "generate the value for this field. If unset, the defaults defined there will be used." 438 ), 439 ), 440 "supported_platform_triples": attr.string_list( 441 doc = "A set of all platform triples to consider when generating dependencies.", 442 default = SUPPORTED_PLATFORM_TRIPLES, 443 ), 444 "vendor_path": attr.string( 445 doc = "The path to a directory to write files into. Absolute paths will be treated as relative to the workspace root", 446 default = "crates", 447 ), 448} 449 450crates_vendor = rule( 451 implementation = _crates_vendor_impl, 452 doc = """\ 453A rule for defining Rust dependencies (crates) and writing targets for them to the current workspace. 454This rule is useful for users whose workspaces are expected to be consumed in other workspaces as the 455rendered `BUILD` files reduce the number of workspace dependencies, allowing for easier loads. This rule 456handles all the same [workflows](#workflows) `crate_universe` rules do. 457 458Example: 459 460Given the following workspace structure: 461 462```text 463[workspace]/ 464 WORKSPACE 465 BUILD 466 Cargo.toml 467 3rdparty/ 468 BUILD 469 src/ 470 main.rs 471``` 472 473The following is something that'd be found in `3rdparty/BUILD`: 474 475```python 476load("@rules_rust//crate_universe:defs.bzl", "crates_vendor", "crate") 477 478crates_vendor( 479 name = "crates_vendor", 480 annotations = { 481 "rand": [crate.annotation( 482 default_features = False, 483 features = ["small_rng"], 484 )], 485 }, 486 cargo_lockfile = "//:Cargo.Bazel.lock", 487 manifests = ["//:Cargo.toml"], 488 mode = "remote", 489 vendor_path = "crates", 490 tags = ["manual"], 491) 492``` 493 494The above creates a target that can be run to write `BUILD` files into the `3rdparty` 495directory next to where the target is defined. To run it, simply call: 496 497```shell 498bazel run //3rdparty:crates_vendor 499``` 500 501<a id="#crates_vendor_repinning_updating_dependencies"></a> 502 503### Repinning / Updating Dependencies 504 505Repinning dependencies is controlled by both the `CARGO_BAZEL_REPIN` environment variable or the `--repin` 506flag to the `crates_vendor` binary. To update dependencies, simply add the flag ro your `bazel run` invocation. 507 508```shell 509bazel run //3rdparty:crates_vendor -- --repin 510``` 511 512Under the hood, `--repin` will trigger a [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html) 513call against the generated workspace. The following table describes how to control particular values passed to the 514`cargo update` command. 515 516| Value | Cargo command | 517| --- | --- | 518| Any of [`true`, `1`, `yes`, `on`, `workspace`] | `cargo update --workspace` | 519| Any of [`full`, `eager`, `all`] | `cargo update` | 520| `package_name` | `cargo upgrade --package package_name` | 521| `[email protected]` | `cargo upgrade --package package_name --precise 1.2.3` | 522 523""", 524 attrs = CRATES_VENDOR_ATTRS, 525 executable = True, 526 toolchains = ["@rules_rust//rust:toolchain_type"], 527) 528 529def _crates_vendor_remote_repository_impl(repository_ctx): 530 build_file = repository_ctx.path(repository_ctx.attr.build_file) 531 defs_module = repository_ctx.path(repository_ctx.attr.defs_module) 532 533 repository_ctx.file("BUILD.bazel", repository_ctx.read(build_file)) 534 repository_ctx.file("defs.bzl", repository_ctx.read(defs_module)) 535 repository_ctx.file("crates.bzl", "") 536 repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( 537 repository_ctx.name, 538 )) 539 540crates_vendor_remote_repository = repository_rule( 541 doc = "Creates a repository paired with `crates_vendor` targets using the `remote` vendor mode.", 542 implementation = _crates_vendor_remote_repository_impl, 543 attrs = { 544 "build_file": attr.label( 545 doc = "The BUILD file to use for the root package", 546 mandatory = True, 547 ), 548 "defs_module": attr.label( 549 doc = "The `defs.bzl` file to use in the repository", 550 mandatory = True, 551 ), 552 }, 553) 554