Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,12 @@ jobs:
with:
tool: cargo-llvm-cov

- name: "Install cargo-binstall"
if: ${{ contains(format(' {0} ', matrix.languages), ' rust ') }}
uses: taiki-e/install-action@288e746965032cfcc232e09af2daf5f23c14d780 # v2.86.1
with:
tool: cargo-binstall

# LuaRocks may compile native Lua modules, such as luafilesystem, on Windows.
- name: "Setup MSVC"
if: ${{ matrix.os == 'windows-latest' && contains(format(' {0} ', matrix.languages), ' lua ') }}
Expand Down
1 change: 1 addition & 0 deletions crates/prek-consts/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ impl EnvVars {

// Rust related
pub const PREK_RUST_PROFILE: &'static str = "PREK_RUST_PROFILE";
pub const PREK_USE_CARGO_BINSTALL: &'static str = "PREK_USE_CARGO_BINSTALL";
pub const RUSTUP_TOOLCHAIN: &'static str = "RUSTUP_TOOLCHAIN";
pub const RUSTUP_AUTO_INSTALL: &'static str = "RUSTUP_AUTO_INSTALL";
pub const CARGO_HOME: &'static str = "CARGO_HOME";
Expand Down
88 changes: 73 additions & 15 deletions crates/prek/src/languages/rust/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::Arc;

use anyhow::{Context, bail};
use itertools::{Either, Itertools};
use prek_consts::env_vars::EnvVars;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_consts::prepend_paths;
use tracing::debug;

Expand Down Expand Up @@ -126,6 +126,46 @@ impl CargoCliDependency {
}
}

enum CargoInstaller<'a> {
Cargo(&'a Path),
Binstall(&'a Path),
}

impl CargoInstaller<'_> {
fn command(&self, install_root: &Path) -> Cmd {
let (executable, args): (&Path, &[&str]) = match self {
Self::Cargo(cargo) => (cargo, &["install", "--bins"]),
Self::Binstall(cargo_binstall) => (cargo_binstall, &["--no-confirm"]),
};
let mut cmd = Cmd::new(executable);
cmd.args(args).arg("--root").arg(install_root);
cmd
}
}

fn cargo_binstall_executable() -> anyhow::Result<Option<PathBuf>> {
let enabled = match EnvVars.var_as_bool(EnvVars::PREK_USE_CARGO_BINSTALL) {
Ok(enabled) => enabled.unwrap_or(false),
Err(value) => {
bail!(
"Invalid value for {}: {value:?}. Expected a boolean value",
EnvVars::PREK_USE_CARGO_BINSTALL,
);
}
};
if !enabled {
return Ok(None);
}

let executable = which::which("cargo-binstall").with_context(|| {
format!(
"{} is enabled, but `cargo-binstall` was not found on PATH",
EnvVars::PREK_USE_CARGO_BINSTALL,
)
})?;
Ok(Some(executable))
}

/// Find the package directory that produces the given binary.
/// Returns (`package_dir`, `package_name`, `is_workspace`).
async fn find_package_dir(
Expand Down Expand Up @@ -264,10 +304,8 @@ async fn install_local_project(
};

if lib_deps.is_empty() {
// For packages without lib deps, use `cargo install` directly
Cmd::new(cargo)
.args(["install", "--bins", "--root"])
.arg(&info.env_path)
CargoInstaller::Cargo(cargo)
.command(&info.env_path)
.args(["--path", ".", "--locked"])
.current_dir(&package_dir)
.env(EnvVars::PATH, new_path)
Expand Down Expand Up @@ -364,25 +402,36 @@ async fn install_cli_dependency(
cli_dep: &str,
info: &InstallInfo,
cargo: &Path,
cargo_binstall: Option<&Path>,
cargo_home: &Path,
new_path: &OsStr,
install_cwd: &Path,
) -> anyhow::Result<()> {
let dep = CargoCliDependency::from_str(cli_dep)?;

let mut cmd = Cmd::new(cargo);
cmd.current_dir(install_cwd)
.args(["install", "--bins", "--root"])
.arg(&info.env_path)
.args(dep.to_cargo_args())
.arg("--locked");
let installer = match (cargo_binstall, &dep) {
(Some(cargo_binstall), CargoCliDependency::Crate { .. }) => {
debug!(
dependency = cli_dep,
"Installing CLI dependency with cargo-binstall"
);
CargoInstaller::Binstall(cargo_binstall)
}
_ => CargoInstaller::Cargo(cargo),
};

cmd.env(EnvVars::PATH, new_path)
installer
.command(&info.env_path)
.args(dep.to_cargo_args())
.arg("--locked")
.current_dir(install_cwd)
.env(EnvVars::PATH, new_path)
.env(EnvVars::CARGO_HOME, cargo_home)
.sanitize_git_repo_env()
.check(true)
.output()
.await?;
.await
.context("Failed to install Rust CLI dependency")?;

Ok(())
}
Expand All @@ -400,6 +449,7 @@ impl LanguageBackend for Rust {
reporter: &HookInstallReporter,
) -> anyhow::Result<InstalledHook> {
let progress = reporter.on_install_start(&hook);
let cargo_binstall = cargo_binstall_executable()?;

// 1. Install Rust
let cargo_home = store.cache_path(CacheBucket::Cargo);
Expand Down Expand Up @@ -470,8 +520,16 @@ impl LanguageBackend for Rust {

// Install CLI dependencies
for cli_dep in cli_deps {
install_cli_dependency(cli_dep, &info, &cargo, &cargo_home, &new_path, install_cwd)
.await?;
install_cli_dependency(
cli_dep,
&info,
&cargo,
cargo_binstall.as_deref(),
&cargo_home,
&new_path,
install_cwd,
)
.await?;
}

info.persist_env_path();
Expand Down
100 changes: 100 additions & 0 deletions crates/prek/tests/languages/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,106 @@ fn additional_dependencies_cli() {
");
}

/// Test cargo-binstall is used only for crates.io CLI dependencies.
#[cfg(feature = "ci")]
#[test]
fn cargo_binstall_is_used_only_for_crates_io_cli_dependencies() {
let context = TestEnv::new()
.with_file(
"Cargo.toml",
indoc::indoc! {r#"
[package]
name = "cargo-binstall-test"
version = "0.1.0"
edition = "2021"
"#},
)
.init_git();
let hook_repo = context
.create_hook_repo(
"rust-cargo-hook",
indoc::indoc! {r"
- id: cargo-hook
name: cargo-hook
language: rust
entry: prek-cargo-hook
"},
)
.with_file(
"Cargo.toml",
indoc::indoc! {r#"
[package]
name = "prek-cargo-hook"
version = "0.1.0"
edition = "2021"
"#},
)
.with_file(
"Cargo.lock",
indoc::indoc! {r#"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "prek-cargo-hook"
version = "0.1.0"
"#},
)
.with_file(
"src/main.rs",
indoc::indoc! {r#"
fn main() {
println!("installed from checkout");
}
"#},
)
.build();

context.write_config(indoc::formatdoc! {r#"
repos:
- repo: {hook_repo}
rev: v1.0.0
hooks:
- id: cargo-hook
language_version: system
always_run: true
verbose: true
pass_filenames: false
- repo: local
hooks:
- id: binstall-cli
name: binstall-cli
language: rust
entry: cargo-get package.name
additional_dependencies: ["cli:cargo-get:1.4.0"]
language_version: system
always_run: true
verbose: true
pass_filenames: false
"#});
context.git().add(".");

cmd_snapshot!(context, context.run()
.env(EnvVars::PREK_USE_CARGO_BINSTALL, "1"), @r#"
success: true
exit_code: 0
----- stdout -----
cargo-hook...............................................................Passed
- hook id: cargo-hook
- duration: [TIME]

installed from checkout
binstall-cli.............................................................Passed
- hook id: binstall-cli
- duration: [TIME]

cargo-binstall-test

----- stderr -----
"#);
}

/// Test that remote Rust hooks are installed and run correctly.
#[test]
fn remote_hooks() {
Expand Down
2 changes: 2 additions & 0 deletions docs/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,8 @@ Gems specified in hook gemspec files and `additional_dependencies` are installed

prek installs binaries via `cargo install --bins --locked` and runs the specified executable. The repository should contain a `Cargo.toml` that produces the binary referenced by `entry`. `additional_dependencies` and `language_version` are supported.

Only crates.io `cli:` dependencies use a preinstalled [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) when [`PREK_USE_CARGO_BINSTALL=1`](reference/environment-variables.md#prek_use_cargo_binstall) is set. prek does not install cargo-binstall or change its telemetry settings. cargo-binstall falls back to compiling from source when it cannot find a suitable binary, unless the user disables its `compile` strategy.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer multiple shorter lines in documentation files (I break in range 80-90 characters or by publication marks or at text "breaks"). In AsciiDoc this is a standard de-facto.


!!! note "Using `--locked` flag"

prek uses the `--locked` flag when installing Rust packages to ensure exact dependency versions from `Cargo.lock` are used. This prevents breaking changes from new dependency releases.
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ See [Ruby language support](../languages.md#ruby) for details.
Override the `rustup` profile used when installing managed Rust toolchains (`minimal`, `default`, or `complete`). Defaults to `minimal`. Set to `default` to include `rustfmt` and `clippy`.
See [Rust language support](../languages.md#rust) for details.

### `PREK_USE_CARGO_BINSTALL`

Use a preinstalled [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) for crates.io `cli:` dependencies of `language: rust` hooks.
prek does not install cargo-binstall or override its telemetry and strategy settings. See [Rust language support](../languages.md#rust) for installations that continue to use Cargo.

## Compatibility fallbacks

### `PRE_COMMIT_ALLOW_NO_CONFIG`
Expand Down
Loading