| """The `cargo_bootstrap` rule is used for bootstrapping cargo binaries in a repository rule.""" |
| |
| load("//cargo/private:cargo_utils.bzl", "get_rust_tools") |
| load("//rust:defs.bzl", "rust_common") |
| load("//rust/platform:triple.bzl", "get_host_triple") |
| |
| _CARGO_BUILD_MODES = [ |
| "release", |
| "debug", |
| ] |
| |
| _FAIL_MESSAGE = """\ |
| Process exited with code '{code}' |
| # ARGV ######################################################################## |
| {argv} |
| |
| # STDOUT ###################################################################### |
| {stdout} |
| |
| # STDERR ###################################################################### |
| {stderr} |
| """ |
| |
| def cargo_bootstrap( |
| repository_ctx, |
| cargo_bin, |
| rustc_bin, |
| binary, |
| cargo_manifest, |
| environment = {}, |
| quiet = False, |
| build_mode = "release", |
| target_dir = None, |
| timeout = 600): |
| """A function for bootstrapping a cargo binary within a repository rule |
| |
| Args: |
| repository_ctx (repository_ctx): The rule's context object. |
| cargo_bin (path): The path to a Cargo binary. |
| rustc_bin (path): The path to a Rustc binary. |
| binary (str): The binary to build (the `--bin` parameter for Cargo). |
| cargo_manifest (path): The path to a Cargo manifest (Cargo.toml file). |
| environment (dict): Environment variables to use during execution. |
| quiet (bool, optional): Whether or not to print output from the Cargo command. |
| build_mode (str, optional): The build mode to use |
| target_dir (path, optional): The directory in which to produce build outputs |
| (Cargo's --target-dir argument). |
| timeout (int, optional): Maximum duration of the Cargo build command in seconds, |
| |
| Returns: |
| path: The path of the built binary within the target directory |
| """ |
| |
| if not target_dir: |
| target_dir = repository_ctx.path(".") |
| |
| args = [ |
| cargo_bin, |
| "build", |
| "--bin", |
| binary, |
| "--locked", |
| "--target-dir", |
| target_dir, |
| "--manifest-path", |
| cargo_manifest, |
| ] |
| |
| if build_mode not in _CARGO_BUILD_MODES: |
| fail("'{}' is not a supported build mode. Use one of {}".format(build_mode, _CARGO_BUILD_MODES)) |
| |
| if build_mode == "release": |
| args.append("--release") |
| |
| env = dict({ |
| "RUSTC": str(rustc_bin), |
| }.items() + environment.items()) |
| |
| repository_ctx.report_progress("Cargo Bootstrapping {}".format(binary)) |
| result = repository_ctx.execute( |
| args, |
| environment = env, |
| quiet = quiet, |
| timeout = timeout, |
| ) |
| |
| if result.return_code != 0: |
| fail(_FAIL_MESSAGE.format( |
| code = result.return_code, |
| argv = args, |
| stdout = result.stdout, |
| stderr = result.stderr, |
| )) |
| |
| extension = "" |
| if "win" in repository_ctx.os.name: |
| extension = ".exe" |
| |
| binary_path = "{}/{}{}".format( |
| build_mode, |
| binary, |
| extension, |
| ) |
| |
| if not repository_ctx.path(binary_path).exists: |
| fail("Failed to produce binary at {}".format(binary_path)) |
| |
| return binary_path |
| |
| _BUILD_FILE_CONTENT = """\ |
| load("@rules_rust//rust:defs.bzl", "rust_binary") |
| |
| package(default_visibility = ["//visibility:public"]) |
| |
| exports_files([ |
| "{binary_name}", |
| "{binary}" |
| ]) |
| |
| alias( |
| name = "binary", |
| actual = "{binary}", |
| ) |
| |
| rust_binary( |
| name = "install", |
| rustc_env = {{ |
| "RULES_RUST_CARGO_BOOTSTRAP_BINARY": "$(rootpath {binary})" |
| }}, |
| data = [ |
| "{binary}", |
| ], |
| srcs = [ |
| "@rules_rust//cargo/bootstrap:bootstrap_installer.rs" |
| ], |
| ) |
| """ |
| |
| def _collect_environ(repository_ctx, host_triple): |
| """Gather environment varialbes to use from the current rule context |
| |
| Args: |
| repository_ctx (repository_ctx): The rule's context object. |
| host_triple (str): A string of the current host triple |
| |
| Returns: |
| dict: A map of environment variables |
| """ |
| env_vars = dict(json.decode(repository_ctx.attr.env.get(host_triple, "{}"))) |
| |
| # Gather the path for each label and ensure it exists |
| env_labels = dict(json.decode(repository_ctx.attr.env_label.get(host_triple, "{}"))) |
| env_labels = {key: repository_ctx.path(Label(value)) for (key, value) in env_labels.items()} |
| for key in env_labels: |
| if not env_labels[key].exists: |
| fail("File for key '{}' does not exist: {}", key, env_labels[key]) |
| env_labels = {key: str(value) for (key, value) in env_labels.items()} |
| |
| return dict(env_vars.items() + env_labels.items()) |
| |
| def _detect_changes(repository_ctx): |
| """Inspect files that are considered inputs to the build for changes |
| |
| Args: |
| repository_ctx (repository_ctx): The rule's context object. |
| """ |
| # Simply generating a `path` object consideres the file as 'tracked' or |
| # 'consumed' which means changes to it will trigger rebuilds |
| |
| for src in repository_ctx.attr.srcs: |
| repository_ctx.path(src) |
| |
| repository_ctx.path(repository_ctx.attr.cargo_lockfile) |
| repository_ctx.path(repository_ctx.attr.cargo_toml) |
| |
| def _cargo_bootstrap_repository_impl(repository_ctx): |
| # Pretend to Bazel that this rule's input files have been used, so that it will re-run the rule if they change. |
| _detect_changes(repository_ctx) |
| |
| # Expects something like `1.56.0`, or `nightly/2021-09-08`. |
| version, _, iso_date = repository_ctx.attr.version.partition("/") |
| if iso_date: |
| channel = version |
| version = iso_date |
| else: |
| channel = "stable" |
| |
| host_triple = get_host_triple(repository_ctx) |
| cargo_template = repository_ctx.attr.rust_toolchain_cargo_template |
| rustc_template = repository_ctx.attr.rust_toolchain_rustc_template |
| |
| tools = get_rust_tools( |
| cargo_template = cargo_template, |
| rustc_template = rustc_template, |
| host_triple = host_triple, |
| channel = channel, |
| version = version, |
| ) |
| |
| binary_name = repository_ctx.attr.binary or repository_ctx.name |
| |
| # In addition to platform specific environment variables, a common set (indicated by `*`) will always |
| # be gathered. |
| environment = dict(_collect_environ(repository_ctx, "*").items() + _collect_environ(repository_ctx, host_triple.str).items()) |
| |
| built_binary = cargo_bootstrap( |
| repository_ctx = repository_ctx, |
| cargo_bin = repository_ctx.path(tools.cargo), |
| rustc_bin = repository_ctx.path(tools.rustc), |
| binary = binary_name, |
| cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml), |
| build_mode = repository_ctx.attr.build_mode, |
| environment = environment, |
| timeout = repository_ctx.attr.timeout, |
| ) |
| |
| # Create a symlink so that the binary can be accesed via it's target name |
| repository_ctx.symlink(built_binary, binary_name) |
| |
| repository_ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT.format( |
| binary_name = binary_name, |
| binary = built_binary, |
| )) |
| |
| cargo_bootstrap_repository = repository_rule( |
| doc = "A rule for bootstrapping a Rust binary using [Cargo](https://doc.rust-lang.org/cargo/)", |
| implementation = _cargo_bootstrap_repository_impl, |
| attrs = { |
| "binary": attr.string( |
| doc = "The binary to build (the `--bin` parameter for Cargo). If left empty, the repository name will be used.", |
| ), |
| "build_mode": attr.string( |
| doc = "The build mode the binary should be built with", |
| values = [ |
| "debug", |
| "release", |
| ], |
| default = "release", |
| ), |
| "cargo_lockfile": attr.label( |
| doc = "The lockfile of the crate_universe resolver", |
| allow_single_file = ["Cargo.lock"], |
| mandatory = True, |
| ), |
| "cargo_toml": attr.label( |
| doc = "The path of the crate_universe resolver manifest (`Cargo.toml` file)", |
| allow_single_file = ["Cargo.toml"], |
| mandatory = True, |
| ), |
| "env": attr.string_dict( |
| doc = ( |
| "A mapping of platform triple to a set of environment variables. See " + |
| "[cargo_env](#cargo_env) for usage details. Additionally, the platform triple `*` applies to all platforms." |
| ), |
| ), |
| "env_label": attr.string_dict( |
| doc = ( |
| "A mapping of platform triple to a set of environment variables. This " + |
| "attribute differs from `env` in that all variables passed here must be " + |
| "fully qualified labels of files. See [cargo_env](#cargo_env) for usage details. " + |
| "Additionally, the platform triple `*` applies to all platforms." |
| ), |
| ), |
| "rust_toolchain_cargo_template": attr.string( |
| doc = ( |
| "The template to use for finding the host `cargo` binary. `{version}` (eg. '1.53.0'), " + |
| "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + |
| "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + |
| "replaced in the string if present." |
| ), |
| default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", |
| ), |
| "rust_toolchain_rustc_template": attr.string( |
| doc = ( |
| "The template to use for finding the host `rustc` binary. `{version}` (eg. '1.53.0'), " + |
| "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + |
| "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + |
| "replaced in the string if present." |
| ), |
| default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", |
| ), |
| "srcs": attr.label_list( |
| doc = "Souce files of the crate to build. Passing source files here can be used to trigger rebuilds when changes are made", |
| allow_files = True, |
| ), |
| "timeout": attr.int( |
| doc = "Maximum duration of the Cargo build command in seconds", |
| default = 600, |
| ), |
| "version": attr.string( |
| doc = "The version of Rust the currently registered toolchain is using. Eg. `1.56.0`, or `nightly/2021-09-08`", |
| default = rust_common.default_version, |
| ), |
| }, |
| ) |
| |
| def cargo_env(env): |
| """A helper for generating platform specific environment variables |
| |
| ```python |
| load("@rules_rust//rust:defs.bzl", "rust_common") |
| load("@rules_rust//cargo:defs.bzl", "cargo_bootstrap_repository", "cargo_env") |
| |
| cargo_bootstrap_repository( |
| name = "bootstrapped_bin", |
| cargo_lockfile = "//:Cargo.lock", |
| cargo_toml = "//:Cargo.toml", |
| srcs = ["//:resolver_srcs"], |
| version = rust_common.default_version, |
| binary = "my-crate-binary", |
| env = { |
| "x86_64-unknown-linux-gnu": cargo_env({ |
| "FOO": "BAR", |
| }), |
| }, |
| env_label = { |
| "aarch64-unknown-linux-musl": cargo_env({ |
| "DOC": "//:README.md", |
| }), |
| } |
| ) |
| ``` |
| |
| Args: |
| env (dict): A map of environment variables |
| |
| Returns: |
| str: A json encoded string of the environment variables |
| """ |
| return json.encode(dict(env)) |