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 packges 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" 208 crate_label_template = "//{}/{{name}}-{{version}}:{{target}}".format( 209 output_pkg, 210 ) 211 else: 212 build_file_base_template = "@{}//{}:BUILD.{{name}}-{{version}}.bazel" 213 crate_label_template = render_config["crate_label_template"] 214 215 updates = { 216 "build_file_template": build_file_base_template.format( 217 workspace_name, 218 output_pkg, 219 ), 220 "crate_label_template": crate_label_template, 221 "crates_module_template": "@{}//{}:{{file}}".format( 222 workspace_name, 223 output_pkg, 224 ), 225 "vendor_mode": mode, 226 } 227 228 # "crate_label_template" is explicitly supported above in non-local modes 229 excluded_from_key_check = ["crate_label_template"] 230 231 for key in updates: 232 if (render_config[key] != default_render_config[key]) and key not in excluded_from_key_check: 233 fail("The `crates_vendor.render_config` attribute does not support the `{}` parameter. Please update {} to remove this value.".format( 234 key, 235 ctx.label, 236 )) 237 238 render_config.update(updates) 239 240 # Allow users to override the regen command. 241 if "regen_command" not in render_config or not render_config["regen_command"]: 242 render_config.update({"regen_command": "bazel run {}".format(ctx.label)}) 243 244 config_data = compile_config( 245 crate_annotations = annotations, 246 generate_binaries = generate_binaries, 247 generate_build_scripts = generate_build_scripts, 248 generate_target_compatible_with = generate_target_compatible_with, 249 cargo_config = None, 250 render_config = render_config, 251 supported_platform_triples = supported_platform_triples, 252 repository_name = repository_name or ctx.label.name, 253 repository_ctx = repository_ctx, 254 ) 255 256 return json.encode_indent( 257 config_data, 258 indent = " " * 4, 259 ) 260 261def _crates_vendor_impl(ctx): 262 toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")] 263 is_windows = _is_windows(ctx) 264 265 environ = { 266 "CARGO": _runfiles_path(toolchain.cargo, is_windows), 267 "RUSTC": _runfiles_path(toolchain.rustc, is_windows), 268 } 269 270 args = ["vendor"] 271 272 cargo_bazel_runfiles = [] 273 274 # Allow action envs to override the use of the cargo-bazel target. 275 if CARGO_BAZEL_GENERATOR_PATH in ctx.var: 276 bin_path = ctx.var[CARGO_BAZEL_GENERATOR_PATH] 277 elif ctx.executable.cargo_bazel: 278 bin_path = _runfiles_path(ctx.executable.cargo_bazel, is_windows) 279 cargo_bazel_runfiles.append(ctx.executable.cargo_bazel) 280 else: 281 fail("{} is missing either the `cargo_bazel` attribute or the '{}' action env".format( 282 ctx.label, 283 CARGO_BAZEL_GENERATOR_PATH, 284 )) 285 286 # Generate config file 287 config_args, config_runfiles = _write_config_file(ctx) 288 args.extend(config_args) 289 cargo_bazel_runfiles.extend(config_runfiles) 290 291 # Generate splicing manifest 292 splicing_manifest_args, splicing_manifest_runfiles = _write_splicing_manifest(ctx) 293 args.extend(splicing_manifest_args) 294 cargo_bazel_runfiles.extend(splicing_manifest_runfiles) 295 296 # Add an optional `Cargo.lock` file. 297 if ctx.attr.cargo_lockfile: 298 args.extend([ 299 "--cargo-lockfile", 300 _runfiles_path(ctx.file.cargo_lockfile, is_windows), 301 ]) 302 cargo_bazel_runfiles.extend([ctx.file.cargo_lockfile]) 303 304 # Optionally include buildifier 305 if ctx.attr.buildifier: 306 args.extend(["--buildifier", _runfiles_path(ctx.executable.buildifier, is_windows)]) 307 cargo_bazel_runfiles.append(ctx.executable.buildifier) 308 309 # Optionally include an explicit `bazel` path 310 if ctx.attr.bazel: 311 args.extend(["--bazel", _runfiles_path(ctx.executable.bazel, is_windows)]) 312 cargo_bazel_runfiles.append(ctx.executable.bazel) 313 314 # Determine platform specific settings 315 if is_windows: 316 extension = ".bat" 317 template = _WINDOWS_WRAPPER 318 env_template = "\nset {}={}" 319 else: 320 extension = ".sh" 321 template = _UNIX_WRAPPER 322 env_template = "{}={}" 323 324 # Write the wrapper script 325 runner = ctx.actions.declare_file(ctx.label.name + extension) 326 ctx.actions.write( 327 output = runner, 328 content = template.format( 329 env = " ".join([env_template.format(key, val) for key, val in environ.items()]), 330 bin = bin_path, 331 args = " ".join(args), 332 ), 333 is_executable = True, 334 ) 335 336 return DefaultInfo( 337 files = depset([runner]), 338 runfiles = ctx.runfiles( 339 files = cargo_bazel_runfiles, 340 transitive_files = toolchain.all_files, 341 ), 342 executable = runner, 343 ) 344 345CRATES_VENDOR_ATTRS = { 346 "annotations": attr.string_list_dict( 347 doc = "Extra settings to apply to crates. See [crate.annotation](#crateannotation).", 348 ), 349 "bazel": attr.label( 350 doc = "The path to a bazel binary used to locate the output_base for the current workspace.", 351 cfg = "exec", 352 executable = True, 353 allow_files = True, 354 ), 355 "buildifier": attr.label( 356 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.", 357 cfg = "exec", 358 executable = True, 359 allow_files = True, 360 default = Label("//crate_universe/private/vendor:buildifier"), 361 ), 362 "cargo_bazel": attr.label( 363 doc = ( 364 "The cargo-bazel binary to use for vendoring. If this attribute is not set, then a " + 365 "`{}` action env will be used.".format(CARGO_BAZEL_GENERATOR_PATH) 366 ), 367 cfg = "exec", 368 executable = True, 369 allow_files = True, 370 default = CARGO_BAZEL_LABEL, 371 ), 372 "cargo_config": attr.label( 373 doc = "A [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) file.", 374 allow_single_file = True, 375 ), 376 "cargo_lockfile": attr.label( 377 doc = "The path to an existing `Cargo.lock` file", 378 allow_single_file = True, 379 ), 380 "generate_binaries": attr.bool( 381 doc = ( 382 "Whether to generate `rust_binary` targets for all the binary crates in every package. " + 383 "By default only the `rust_library` targets are generated." 384 ), 385 default = False, 386 ), 387 "generate_build_scripts": attr.bool( 388 doc = ( 389 "Whether or not to generate " + 390 "[cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) by default." 391 ), 392 default = True, 393 ), 394 "generate_target_compatible_with": attr.bool( 395 doc = "DEPRECATED: Moved to `render_config`.", 396 default = True, 397 ), 398 "manifests": attr.label_list( 399 doc = "A list of Cargo manifests (`Cargo.toml` files).", 400 allow_files = ["Cargo.toml"], 401 ), 402 "mode": attr.string( 403 doc = ( 404 "Flags determining how crates should be vendored. `local` is where crate source and BUILD files are " + 405 "written to the repository. `remote` is where only BUILD files are written and repository rules " + 406 "used to fetch source code." 407 ), 408 values = [ 409 "local", 410 "remote", 411 ], 412 default = "remote", 413 ), 414 "packages": attr.string_dict( 415 doc = "A set of crates (packages) specifications to depend on. See [crate.spec](#crate.spec).", 416 ), 417 "render_config": attr.string( 418 doc = ( 419 "The configuration flags to use for rendering. Use `//crate_universe:defs.bzl\\%render_config` to " + 420 "generate the value for this field. If unset, the defaults defined there will be used." 421 ), 422 ), 423 "repository_name": attr.string( 424 doc = "The name of the repository to generate for `remote` vendor modes. If unset, the label name will be used", 425 ), 426 "splicing_config": attr.string( 427 doc = ( 428 "The configuration flags to use for splicing Cargo maniests. Use `//crate_universe:defs.bzl\\%rsplicing_config` to " + 429 "generate the value for this field. If unset, the defaults defined there will be used." 430 ), 431 ), 432 "supported_platform_triples": attr.string_list( 433 doc = "A set of all platform triples to consider when generating dependencies.", 434 default = SUPPORTED_PLATFORM_TRIPLES, 435 ), 436 "vendor_path": attr.string( 437 doc = "The path to a directory to write files into. Absolute paths will be treated as relative to the workspace root", 438 default = "crates", 439 ), 440} 441 442crates_vendor = rule( 443 implementation = _crates_vendor_impl, 444 doc = """\ 445A rule for defining Rust dependencies (crates) and writing targets for them to the current workspace. 446This rule is useful for users whose workspaces are expected to be consumed in other workspaces as the 447rendered `BUILD` files reduce the number of workspace dependencies, allowing for easier loads. This rule 448handles all the same [workflows](#workflows) `crate_universe` rules do. 449 450Example: 451 452Given the following workspace structure: 453 454```text 455[workspace]/ 456 WORKSPACE 457 BUILD 458 Cargo.toml 459 3rdparty/ 460 BUILD 461 src/ 462 main.rs 463``` 464 465The following is something that'd be found in `3rdparty/BUILD`: 466 467```python 468load("@rules_rust//crate_universe:defs.bzl", "crates_vendor", "crate") 469 470crates_vendor( 471 name = "crates_vendor", 472 annotations = { 473 "rand": [crate.annotation( 474 default_features = False, 475 features = ["small_rng"], 476 )], 477 }, 478 cargo_lockfile = "//:Cargo.Bazel.lock", 479 manifests = ["//:Cargo.toml"], 480 mode = "remote", 481 vendor_path = "crates", 482 tags = ["manual"], 483) 484``` 485 486The above creates a target that can be run to write `BUILD` files into the `3rdparty` 487directory next to where the target is defined. To run it, simply call: 488 489```shell 490bazel run //3rdparty:crates_vendor 491``` 492 493<a id="#crates_vendor_repinning_updating_dependencies"></a> 494 495### Repinning / Updating Dependencies 496 497Repinning dependencies is controlled by both the `CARGO_BAZEL_REPIN` environment variable or the `--repin` 498flag to the `crates_vendor` binary. To update dependencies, simply add the flag ro your `bazel run` invocation. 499 500```shell 501bazel run //3rdparty:crates_vendor -- --repin 502``` 503 504Under the hood, `--repin` will trigger a [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html) 505call against the generated workspace. The following table describes how to control particular values passed to the 506`cargo update` command. 507 508| Value | Cargo command | 509| --- | --- | 510| Any of [`true`, `1`, `yes`, `on`, `workspace`] | `cargo update --workspace` | 511| Any of [`full`, `eager`, `all`] | `cargo update` | 512| `package_name` | `cargo upgrade --package package_name` | 513| `package_name@1.2.3` | `cargo upgrade --package package_name --precise 1.2.3` | 514 515""", 516 attrs = CRATES_VENDOR_ATTRS, 517 executable = True, 518 toolchains = ["@rules_rust//rust:toolchain_type"], 519) 520 521def _crates_vendor_remote_repository_impl(repository_ctx): 522 build_file = repository_ctx.path(repository_ctx.attr.build_file) 523 defs_module = repository_ctx.path(repository_ctx.attr.defs_module) 524 525 repository_ctx.file("BUILD.bazel", repository_ctx.read(build_file)) 526 repository_ctx.file("defs.bzl", repository_ctx.read(defs_module)) 527 repository_ctx.file("crates.bzl", "") 528 repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( 529 repository_ctx.name, 530 )) 531 532crates_vendor_remote_repository = repository_rule( 533 doc = "Creates a repository paired with `crates_vendor` targets using the `remote` vendor mode.", 534 implementation = _crates_vendor_remote_repository_impl, 535 attrs = { 536 "build_file": attr.label( 537 doc = "The BUILD file to use for the root package", 538 mandatory = True, 539 ), 540 "defs_module": attr.label( 541 doc = "The `defs.bzl` file to use in the repository", 542 mandatory = True, 543 ), 544 }, 545) 546