| """Rules for vendoring Bazel targets into existing workspaces""" |
| |
| load("//crate_universe/private:generate_utils.bzl", "compile_config", "render_config") |
| load("//crate_universe/private:splicing_utils.bzl", "kebab_case_keys", "splicing_config") |
| load("//crate_universe/private:urls.bzl", "CARGO_BAZEL_LABEL") |
| load("//rust/platform:triple_mappings.bzl", "SUPPORTED_PLATFORM_TRIPLES") |
| |
| _UNIX_WRAPPER = """\ |
| #!/usr/bin/env bash |
| set -euo pipefail |
| export RUNTIME_PWD="$(pwd)" |
| if [[ -z "${{BAZEL_REAL:-}}" ]]; then |
| BAZEL_REAL="$(which bazel || echo 'bazel')" |
| fi |
| eval exec env - BAZEL_REAL="${{BAZEL_REAL}}" BUILD_WORKSPACE_DIRECTORY="${{BUILD_WORKSPACE_DIRECTORY}}" {env} \\ |
| "{bin}" {args} "$@" |
| """ |
| |
| _WINDOWS_WRAPPER = """\ |
| @ECHO OFF |
| set RUNTIME_PWD=%CD% |
| {env} |
| |
| call {bin} {args} %@% |
| """ |
| |
| CARGO_BAZEL_GENERATOR_PATH = "CARGO_BAZEL_GENERATOR_PATH" |
| |
| def _runfiles_path(path, is_windows): |
| if is_windows: |
| runtime_pwd_var = "%RUNTIME_PWD%" |
| else: |
| runtime_pwd_var = "${RUNTIME_PWD}" |
| if path.startswith("../"): |
| return "{}/external/{}".format(runtime_pwd_var, path[len("../"):]) |
| return "{}/{}".format(runtime_pwd_var, path) |
| |
| def _is_windows(ctx): |
| toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain")] |
| return "windows" in toolchain.target_triple |
| |
| def _get_output_package(ctx): |
| # Determine output directory |
| if ctx.attr.vendor_path.startswith("/"): |
| output = ctx.attr.vendor_path |
| else: |
| output = "{}/{}".format( |
| ctx.label.package, |
| ctx.attr.vendor_path, |
| ) |
| return output |
| |
| def _write_data_file(ctx, name, data): |
| file = ctx.actions.declare_file("{}.{}".format(ctx.label.name, name)) |
| ctx.actions.write( |
| output = file, |
| content = data, |
| ) |
| return file |
| |
| def _prepare_manifest_path(target): |
| """Generate manifest paths that are resolvable by `cargo_bazel::SplicingManifest::resolve` |
| |
| Args: |
| target (Target): A `crate_vendor.manifest` target |
| |
| Returns: |
| str: A string representing the path to a manifest. |
| """ |
| files = target[DefaultInfo].files.to_list() |
| if len(files) != 1: |
| fail("The manifest {} hand an unexpected number of files: {}".format( |
| target.label, |
| files, |
| )) |
| |
| manifest = files[0] |
| |
| if target.label.workspace_root.startswith("external"): |
| # The short path of an external file is expected to start with `../` |
| if not manifest.short_path.startswith("../"): |
| fail("Unexpected shortpath for {}: {}".format( |
| manifest, |
| manifest.short_path, |
| )) |
| return manifest.short_path.replace("../", "${output_base}/external/", 1) |
| |
| return "${build_workspace_directory}/" + manifest.short_path |
| |
| def _write_splicing_manifest(ctx): |
| # Deserialize information about direct packges |
| direct_packages_info = { |
| # Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects. |
| pkg: kebab_case_keys(dict(json.decode(data))) |
| for (pkg, data) in ctx.attr.packages.items() |
| } |
| |
| # Manifests are required to be single files |
| manifests = {_prepare_manifest_path(m): str(m.label) for m in ctx.attr.manifests} |
| |
| config = json.decode(ctx.attr.splicing_config or splicing_config()) |
| splicing_manifest_content = { |
| "cargo_config": _prepare_manifest_path(ctx.attr.cargo_config) if ctx.attr.cargo_config else None, |
| "direct_packages": direct_packages_info, |
| "manifests": manifests, |
| } |
| |
| manifest = _write_data_file( |
| ctx = ctx, |
| name = "cargo-bazel-splicing-manifest.json", |
| data = json.encode_indent( |
| dict(dict(config).items() + splicing_manifest_content.items()), |
| indent = " " * 4, |
| ), |
| ) |
| |
| is_windows = _is_windows(ctx) |
| |
| args = ["--splicing-manifest", _runfiles_path(manifest.short_path, is_windows)] |
| runfiles = [manifest] + ctx.files.manifests + ([ctx.file.cargo_config] if ctx.attr.cargo_config else []) |
| return args, runfiles |
| |
| def _write_config_file(ctx): |
| rendering_config = dict(json.decode(render_config( |
| regen_command = "bazel run {}".format( |
| ctx.label, |
| ), |
| ))) |
| |
| output_pkg = _get_output_package(ctx) |
| |
| if ctx.attr.mode == "local": |
| build_file_base_template = "@{}//{}/{{name}}-{{version}}:BUILD.bazel" |
| crate_label_template = "//{}/{{name}}-{{version}}:{{target}}".format( |
| output_pkg, |
| ) |
| else: |
| build_file_base_template = "@{}//{}:BUILD.{{name}}-{{version}}.bazel" |
| crate_label_template = rendering_config["crate_label_template"] |
| |
| rendering_config.update({ |
| "build_file_template": build_file_base_template.format( |
| ctx.workspace_name, |
| output_pkg, |
| ), |
| "crate_label_template": crate_label_template, |
| "crates_module_template": "@{}//{}:{{file}}".format( |
| ctx.workspace_name, |
| output_pkg, |
| ), |
| "vendor_mode": ctx.attr.mode, |
| }) |
| |
| config_data = compile_config( |
| crate_annotations = ctx.attr.annotations, |
| generate_build_scripts = ctx.attr.generate_build_scripts, |
| cargo_config = None, |
| render_config = rendering_config, |
| supported_platform_triples = ctx.attr.supported_platform_triples, |
| repository_name = ctx.attr.repository_name or ctx.label.name, |
| ) |
| |
| config = _write_data_file( |
| ctx = ctx, |
| name = "cargo-bazel-config.json", |
| data = json.encode_indent( |
| config_data, |
| indent = " " * 4, |
| ), |
| ) |
| |
| is_windows = _is_windows(ctx) |
| args = ["--config", _runfiles_path(config.short_path, is_windows)] |
| runfiles = [config] + ctx.files.manifests |
| return args, runfiles |
| |
| def _crates_vendor_impl(ctx): |
| toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain")] |
| is_windows = _is_windows(ctx) |
| |
| environ = { |
| "CARGO": _runfiles_path(toolchain.cargo.short_path, is_windows), |
| "RUSTC": _runfiles_path(toolchain.rustc.short_path, is_windows), |
| } |
| |
| args = ["vendor"] |
| |
| cargo_bazel_runfiles = [] |
| |
| # Allow action envs to override the use of the cargo-bazel target. |
| if CARGO_BAZEL_GENERATOR_PATH in ctx.var: |
| bin_path = ctx.var[CARGO_BAZEL_GENERATOR_PATH] |
| elif ctx.executable.cargo_bazel: |
| bin_path = _runfiles_path(ctx.executable.cargo_bazel.short_path, is_windows) |
| cargo_bazel_runfiles.append(ctx.executable.cargo_bazel) |
| else: |
| fail("{} is missing either the `cargo_bazel` attribute or the '{}' action env".format( |
| ctx.label, |
| CARGO_BAZEL_GENERATOR_PATH, |
| )) |
| |
| # Generate config file |
| config_args, config_runfiles = _write_config_file(ctx) |
| args.extend(config_args) |
| cargo_bazel_runfiles.extend(config_runfiles) |
| |
| # Generate splicing manifest |
| splicing_manifest_args, splicing_manifest_runfiles = _write_splicing_manifest(ctx) |
| args.extend(splicing_manifest_args) |
| cargo_bazel_runfiles.extend(splicing_manifest_runfiles) |
| |
| # Add an optional `Cargo.lock` file. |
| if ctx.attr.cargo_lockfile: |
| args.extend([ |
| "--cargo-lockfile", |
| _runfiles_path(ctx.file.cargo_lockfile.short_path, is_windows), |
| ]) |
| cargo_bazel_runfiles.extend([ctx.file.cargo_lockfile]) |
| |
| # Optionally include buildifier |
| if ctx.attr.buildifier: |
| args.extend(["--buildifier", _runfiles_path(ctx.executable.buildifier.short_path, is_windows)]) |
| cargo_bazel_runfiles.append(ctx.executable.buildifier) |
| |
| # Dtermine platform specific settings |
| if is_windows: |
| extension = ".bat" |
| template = _WINDOWS_WRAPPER |
| env_template = "\nset {}={}" |
| else: |
| extension = ".sh" |
| template = _UNIX_WRAPPER |
| env_template = "{}={}" |
| |
| # Write the wrapper script |
| runner = ctx.actions.declare_file(ctx.label.name + extension) |
| ctx.actions.write( |
| output = runner, |
| content = template.format( |
| env = " ".join([env_template.format(key, val) for key, val in environ.items()]), |
| bin = bin_path, |
| args = " ".join(args), |
| ), |
| is_executable = True, |
| ) |
| |
| return DefaultInfo( |
| files = depset([runner]), |
| runfiles = ctx.runfiles( |
| files = cargo_bazel_runfiles, |
| transitive_files = toolchain.all_files, |
| ), |
| executable = runner, |
| ) |
| |
| crates_vendor = rule( |
| implementation = _crates_vendor_impl, |
| doc = """\ |
| A rule for defining Rust dependencies (crates) and writing targets for them to the current workspace. |
| This rule is useful for users whose workspaces are expected to be consumed in other workspaces as the |
| rendered `BUILD` files reduce the number of workspace dependencies, allowing for easier loads. This rule |
| handles all the same [workflows](#workflows) `crate_universe` rules do. |
| |
| Example: |
| |
| Given the following workspace structure: |
| |
| ```text |
| [workspace]/ |
| WORKSPACE |
| BUILD |
| Cargo.toml |
| 3rdparty/ |
| BUILD |
| src/ |
| main.rs |
| ``` |
| |
| The following is something that'd be found in `3rdparty/BUILD`: |
| |
| ```python |
| load("@rules_rust//crate_universe:defs.bzl", "crates_vendor", "crate") |
| |
| crates_vendor( |
| name = "crates_vendor", |
| annotations = { |
| "rand": [crate.annotation( |
| default_features = False, |
| features = ["small_rng"], |
| )], |
| }, |
| cargo_lockfile = "//:Cargo.Bazel.lock", |
| manifests = ["//:Cargo.toml"], |
| mode = "remote", |
| vendor_path = "crates", |
| tags = ["manual"], |
| ) |
| ``` |
| |
| The above creates a target that can be run to write `BUILD` files into the `3rdparty` |
| directory next to where the target is defined. To run it, simply call: |
| |
| ```shell |
| bazel run //3rdparty:crates_vendor |
| ``` |
| |
| <a id="#crates_vendor_repinning_updating_dependencies"></a> |
| |
| ### Repinning / Updating Dependencies |
| |
| Repinning dependencies is controlled by both the `CARGO_BAZEL_REPIN` environment variable or the `--repin` |
| flag to the `crates_vendor` binary. To update dependencies, simply add the flag ro your `bazel run` invocation. |
| |
| ```shell |
| bazel run //3rdparty:crates_vendor -- --repin |
| ``` |
| |
| Under the hood, `--repin` will trigger a [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html) |
| call against the generated workspace. The following table describes how to controll particular values passed to the |
| `cargo update` command. |
| |
| | Value | Cargo command | |
| | --- | --- | |
| | Any of [`true`, `1`, `yes`, `on`] | `cargo update` | |
| | `workspace` | `cargo update --workspace` | |
| | `package_name` | `cargo upgrade --package package_name` | |
| | `package_name@1.2.3` | `cargo upgrade --package package_name --precise 1.2.3` | |
| |
| """, |
| attrs = { |
| "annotations": attr.string_list_dict( |
| doc = "Extra settings to apply to crates. See [crate.annotation](#crateannotation).", |
| ), |
| "buildifier": attr.label( |
| 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.", |
| cfg = "exec", |
| executable = True, |
| default = Label("//crate_universe/private/vendor:buildifier"), |
| ), |
| "cargo_bazel": attr.label( |
| doc = ( |
| "The cargo-bazel binary to use for vendoring. If this attribute is not set, then a " + |
| "`{}` action env will be used.".format(CARGO_BAZEL_GENERATOR_PATH) |
| ), |
| cfg = "exec", |
| executable = True, |
| allow_files = True, |
| default = CARGO_BAZEL_LABEL, |
| ), |
| "cargo_config": attr.label( |
| doc = "A [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) file.", |
| allow_single_file = True, |
| ), |
| "cargo_lockfile": attr.label( |
| doc = "The path to an existing `Cargo.lock` file", |
| allow_single_file = True, |
| ), |
| "generate_build_scripts": attr.bool( |
| doc = ( |
| "Whether or not to generate " + |
| "[cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) by default." |
| ), |
| default = True, |
| ), |
| "manifests": attr.label_list( |
| doc = "A list of Cargo manifests (`Cargo.toml` files).", |
| allow_files = ["Cargo.toml"], |
| ), |
| "mode": attr.string( |
| doc = ( |
| "Flags determining how crates should be vendored. `local` is where crate source and BUILD files are " + |
| "written to the repository. `remote` is where only BUILD files are written and repository rules " + |
| "used to fetch source code." |
| ), |
| values = [ |
| "local", |
| "remote", |
| ], |
| default = "remote", |
| ), |
| "packages": attr.string_dict( |
| doc = "A set of crates (packages) specifications to depend on. See [crate.spec](#crate.spec).", |
| ), |
| "repository_name": attr.string( |
| doc = "The name of the repository to generate for `remote` vendor modes. If unset, the label name will be used", |
| ), |
| "splicing_config": attr.string( |
| doc = ( |
| "The configuration flags to use for splicing Cargo maniests. Use `//crate_universe:defs.bzl\\%rsplicing_config` to " + |
| "generate the value for this field. If unset, the defaults defined there will be used." |
| ), |
| ), |
| "supported_platform_triples": attr.string_list( |
| doc = "A set of all platform triples to consider when generating dependencies.", |
| default = SUPPORTED_PLATFORM_TRIPLES, |
| ), |
| "vendor_path": attr.string( |
| doc = "The path to a directory to write files into. Absolute paths will be treated as relative to the workspace root", |
| default = "crates", |
| ), |
| }, |
| executable = True, |
| toolchains = ["@rules_rust//rust:toolchain"], |
| ) |
| |
| def _crates_vendor_remote_repository_impl(repository_ctx): |
| build_file = repository_ctx.path(repository_ctx.attr.build_file) |
| defs_module = repository_ctx.path(repository_ctx.attr.defs_module) |
| |
| repository_ctx.file("BUILD.bazel", repository_ctx.read(build_file)) |
| repository_ctx.file("defs.bzl", repository_ctx.read(defs_module)) |
| repository_ctx.file("crates.bzl", "") |
| repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( |
| repository_ctx.name, |
| )) |
| |
| crates_vendor_remote_repository = repository_rule( |
| doc = "Creates a repository paired with `crates_vendor` targets using the `remote` vendor mode.", |
| implementation = _crates_vendor_remote_repository_impl, |
| attrs = { |
| "build_file": attr.label( |
| doc = "The BUILD file to use for the root package", |
| mandatory = True, |
| ), |
| "defs_module": attr.label( |
| doc = "The `defs.bzl` file to use in the repository", |
| mandatory = True, |
| ), |
| }, |
| ) |