Added `cargo_bootstrap_repository` rule (#891)
* Added `cargo_bootstrap_repository` rule.
* Regenerate documentation
diff --git a/cargo/BUILD.bazel b/cargo/BUILD.bazel
index d4dd2d5..c0590c0 100644
--- a/cargo/BUILD.bazel
+++ b/cargo/BUILD.bazel
@@ -5,4 +5,5 @@
bzl_library(
name = "rules",
srcs = glob(["**/*.bzl"]),
+ deps = ["//cargo/private:bzl_lib"],
)
diff --git a/cargo/bootstrap/BUILD.bazel b/cargo/bootstrap/BUILD.bazel
new file mode 100644
index 0000000..a607f60
--- /dev/null
+++ b/cargo/bootstrap/BUILD.bazel
@@ -0,0 +1,20 @@
+load("//rust:defs.bzl", "rust_binary")
+
+exports_files(
+ [
+ "bootstrap_installer.rs",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+# This target is only used to confirm the source code is buildable
+# in a `cargo_bootstrap_repository` rule.
+rust_binary(
+ name = "bootstrap_installer_bin",
+ srcs = [
+ "bootstrap_installer.rs",
+ ],
+ rustc_env = {
+ "RULES_RUST_CARGO_BOOTSTRAP_BINARY": "$(rootpath bootstrap_installer.rs)",
+ },
+)
diff --git a/cargo/bootstrap/bootstrap_installer.rs b/cargo/bootstrap/bootstrap_installer.rs
new file mode 100644
index 0000000..32cb137
--- /dev/null
+++ b/cargo/bootstrap/bootstrap_installer.rs
@@ -0,0 +1,35 @@
+//! A tool for installing bootstrapped Rust binaries into the requested paths.
+
+use std::{
+ env,
+ fs::{copy, create_dir_all},
+ path::PathBuf,
+};
+
+fn install() -> std::io::Result<u64> {
+ let binary = PathBuf::from(env!("RULES_RUST_CARGO_BOOTSTRAP_BINARY"));
+
+ // Consume only the first argument as the destination
+ let dest = PathBuf::from(
+ env::args()
+ .nth(1)
+ .expect("No destination argument provided"),
+ );
+
+ // Create the parent directory structure if it doesn't exist
+ if let Some(parent) = dest.parent() {
+ if !parent.exists() {
+ create_dir_all(parent)?;
+ }
+ }
+
+ // Copy the file to the requested destination
+ copy(binary, dest)
+}
+
+fn main() {
+ if let Err(err) = install() {
+ eprintln!("{:?}", err);
+ std::process::exit(1);
+ };
+}
diff --git a/cargo/cargo_bootstrap.bzl b/cargo/cargo_bootstrap.bzl
new file mode 100644
index 0000000..73b22f8
--- /dev/null
+++ b/cargo/cargo_bootstrap.bzl
@@ -0,0 +1,193 @@
+"""The `cargo_bootstrap` rule is used for bootstrapping cargo binaries in a repository rule."""
+
+load("//cargo/private:cargo_utils.bzl", "get_cargo_and_rustc", "get_host_triple")
+load("//rust:repositories.bzl", "DEFAULT_RUST_VERSION")
+
+_CARGO_BUILD_MODES = [
+ "release",
+ "debug",
+]
+
+def cargo_bootstrap(
+ repository_ctx,
+ cargo_bin,
+ rustc_bin,
+ binary,
+ cargo_manifest,
+ quiet = False,
+ build_mode = "release",
+ target_dir = None):
+ """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).
+ 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).
+
+ 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")
+
+ repository_ctx.report_progress("Cargo Bootstrapping {}".format(binary))
+ result = repository_ctx.execute(
+ args,
+ environment = {
+ "RUSTC": str(rustc_bin),
+ },
+ quiet = quiet,
+ )
+
+ if result.return_code != 0:
+ fail("exit_code: {}".format(
+ result.return_code,
+ ))
+
+ 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}"
+])
+
+rust_binary(
+ name = "install",
+ rustc_env = {{
+ "RULES_RUST_CARGO_BOOTSTRAP_BINARY": "$(rootpath {binary})"
+ }},
+ data = [
+ "{binary}",
+ ],
+ srcs = [
+ "@rules_rust//cargo/bootstrap:bootstrap_installer.rs"
+ ],
+)
+"""
+
+def _cargo_bootstrap_repository_impl(repository_ctx):
+ if repository_ctx.attr.version in ("beta", "nightly"):
+ version_str = "{}-{}".format(repository_ctx.attr.version, repository_ctx.attr.iso_date)
+ else:
+ version_str = repository_ctx.attr.version
+
+ host_triple = get_host_triple(repository_ctx)
+ tools = get_cargo_and_rustc(
+ repository_ctx = repository_ctx,
+ toolchain_repository_template = repository_ctx.attr.rust_toolchain_repository_template,
+ host_triple = host_triple,
+ version = version_str,
+ )
+
+ binary_name = repository_ctx.attr.binary or repository_ctx.name
+
+ built_binary = cargo_bootstrap(
+ repository_ctx,
+ cargo_bin = tools.cargo,
+ rustc_bin = tools.rustc,
+ binary = binary_name,
+ cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml),
+ build_mode = repository_ctx.attr.build_mode,
+ )
+
+ # 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,
+ ),
+ "iso_date": attr.string(
+ doc = "The iso_date of cargo binary the resolver should use. Note: This can only be set if `version` is `beta` or `nightly`",
+ ),
+ "rust_toolchain_repository_template": attr.string(
+ doc = (
+ "The template to use for finding the host `rust_toolchain` repository. `{version}` (eg. '1.53.0'), " +
+ "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{system}` (eg. 'darwin'), and `{arch}` (eg. 'aarch64') " +
+ "will be replaced in the string if present."
+ ),
+ default = "rust_{system}_{arch}",
+ ),
+ "srcs": attr.label_list(
+ doc = "Souces to crate to build.",
+ allow_files = True,
+ mandatory = True,
+ ),
+ "version": attr.string(
+ doc = "The version of cargo the resolver should use",
+ default = DEFAULT_RUST_VERSION,
+ ),
+ "_cc_toolchain": attr.label(
+ default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+ ),
+ },
+)
diff --git a/cargo/defs.bzl b/cargo/defs.bzl
new file mode 100644
index 0000000..ed5e31b
--- /dev/null
+++ b/cargo/defs.bzl
@@ -0,0 +1,7 @@
+"""Common definitions for the `@rules_rust//cargo` package"""
+
+load(":cargo_bootstrap.bzl", _cargo_bootstrap_repository = "cargo_bootstrap_repository")
+load(":cargo_build_script.bzl", _cargo_build_script = "cargo_build_script")
+
+cargo_bootstrap_repository = _cargo_bootstrap_repository
+cargo_build_script = _cargo_build_script
diff --git a/cargo/private/BUILD.bazel b/cargo/private/BUILD.bazel
new file mode 100644
index 0000000..6a1aab6
--- /dev/null
+++ b/cargo/private/BUILD.bazel
@@ -0,0 +1,7 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+bzl_library(
+ name = "bzl_lib",
+ srcs = glob(["**/*.bzl"]),
+ visibility = ["//:__subpackages__"],
+)
diff --git a/cargo/private/cargo_utils.bzl b/cargo/private/cargo_utils.bzl
new file mode 100644
index 0000000..e3702bc
--- /dev/null
+++ b/cargo/private/cargo_utils.bzl
@@ -0,0 +1,147 @@
+"""Utility functions for the cargo rules"""
+
+load("//rust/platform:triple.bzl", "triple")
+load(
+ "//rust/platform:triple_mappings.bzl",
+ "system_to_binary_ext",
+)
+
+_CPU_ARCH_ERROR_MSG = """\
+Command failed with exit code '{code}': {args}
+----------stdout:
+{stdout}
+----------stderr:
+{stderr}
+"""
+
+def _query_cpu_architecture(repository_ctx, expected_archs, is_windows = False):
+ """Detect the host CPU architecture
+
+ Args:
+ repository_ctx (repository_ctx): The repository rule's context object
+ expected_archs (list): A list of expected architecture strings
+ is_windows (bool, optional): If true, the cpu lookup will use the windows method (`wmic` vs `uname`)
+
+ Returns:
+ str: The host's CPU architecture
+ """
+ if is_windows:
+ arguments = ["wmic", "os", "get", "osarchitecture"]
+ else:
+ arguments = ["uname", "-m"]
+
+ result = repository_ctx.execute(arguments)
+
+ if result.return_code:
+ fail(_CPU_ARCH_ERROR_MSG.format(
+ code = result.return_code,
+ args = arguments,
+ stdout = result.stdout,
+ stderr = result.stderr,
+ ))
+
+ if is_windows:
+ # Example output:
+ # OSArchitecture
+ # 64-bit
+ lines = result.stdout.split("\n")
+ arch = lines[1].strip()
+
+ # Translate 64-bit to a compatible rust platform
+ # https://doc.rust-lang.org/nightly/rustc/platform-support.html
+ if arch == "64-bit":
+ arch = "x86_64"
+ else:
+ arch = result.stdout.strip("\n")
+
+ # Correct the arm architecture for macos
+ if "mac" in repository_ctx.os.name and arch == "arm64":
+ arch = "aarch64"
+
+ if not arch in expected_archs:
+ fail("{} is not a expected cpu architecture {}\n{}".format(
+ arch,
+ expected_archs,
+ result.stdout,
+ ))
+
+ return arch
+
+def get_host_triple(repository_ctx, abi = None):
+ """Query host information for the appropriate triples for the crate_universe resolver
+
+ Args:
+ repository_ctx (repository_ctx): The rule's repository_ctx
+ abi (str): Since there's no consistent way to check for ABI, this info
+ may be explicitly provided
+
+ Returns:
+ struct: A triple struct, see `@rules_rust//rust/platform:triple.bzl`
+ """
+
+ # Detect the host's cpu architecture
+
+ supported_architectures = {
+ "linux": ["aarch64", "x86_64"],
+ "macos": ["aarch64", "x86_64"],
+ "windows": ["x86_64"],
+ }
+
+ if "linux" in repository_ctx.os.name:
+ cpu = _query_cpu_architecture(repository_ctx, supported_architectures["linux"])
+ return triple("{}-unknown-linux-{}".format(
+ cpu,
+ abi or "gnu",
+ ))
+
+ if "mac" in repository_ctx.os.name:
+ cpu = _query_cpu_architecture(repository_ctx, supported_architectures["macos"])
+ return triple("{}-apple-darwin".format(cpu))
+
+ if "win" in repository_ctx.os.name:
+ cpu = _query_cpu_architecture(repository_ctx, supported_architectures["windows"], True)
+ return triple("{}-pc-windows-{}".format(
+ cpu,
+ abi or "msvc",
+ ))
+
+ fail("Unhandled host os: {}", repository_ctx.os.name)
+
+def get_cargo_and_rustc(repository_ctx, toolchain_repository_template, host_triple, version):
+ """Retrieve a cargo and rustc binary based on the host triple.
+
+ Args:
+ repository_ctx (repository_ctx): The rule's context object
+ toolchain_repository_template (str): A template used to identify the host `rust_toolchain_repository`.
+ host_triple (struct): The host's triple. See `@rules_rust//rust/platform:triple.bzl`.
+ version (str): The version of Cargo+Rustc to use.
+
+ Returns:
+ struct: A struct containing the expected tools
+ """
+
+ rust_toolchain_repository = toolchain_repository_template
+ rust_toolchain_repository = rust_toolchain_repository.replace("{version}", version)
+ rust_toolchain_repository = rust_toolchain_repository.replace("{triple}", host_triple.triple)
+
+ if host_triple.arch:
+ rust_toolchain_repository = rust_toolchain_repository.replace("{arch}", host_triple.arch)
+
+ if host_triple.vendor:
+ rust_toolchain_repository = rust_toolchain_repository.replace("{vendor}", host_triple.vendor)
+
+ if host_triple.system:
+ rust_toolchain_repository = rust_toolchain_repository.replace("{system}", host_triple.system)
+
+ if host_triple.abi:
+ rust_toolchain_repository = rust_toolchain_repository.replace("{abi}", host_triple.abi)
+
+ extension = system_to_binary_ext(host_triple.system)
+
+ cargo_path = repository_ctx.path(Label("@{}{}".format(rust_toolchain_repository, "//:bin/cargo" + extension)))
+ rustc_path = repository_ctx.path(Label("@{}{}".format(rust_toolchain_repository, "//:bin/rustc" + extension)))
+
+ return struct(
+ cargo = cargo_path,
+ rustc = rustc_path,
+ )