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,
+    )