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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* `init` command to set up custom per-project Prep configuration. ([#23] by [@xStrom])
* `copyright` command to easily verify that all Rust source files have correct copyright headers. ([#22] by [@xStrom])
* `--crates <main|aux|all>` option to the `clippy` command. ([#18] by [@xStrom])
* Automatic Cargo installation via `rustup`. ([#24] by [@xStrom])
* Ability to run from within a sub-directory of a workspace as opposed to just from the workspace root. ([#23] by [@xStrom])

### Changed
Expand All @@ -29,6 +30,7 @@
[#18]: https://github.com/Nevermore/prep/pull/18
[#22]: https://github.com/Nevermore/prep/pull/22
[#23]: https://github.com/Nevermore/prep/pull/23
[#24]: https://github.com/Nevermore/prep/pull/24

[Unreleased]: https://github.com/Nevermore/prep/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/Nevermore/prep/compare/v0.0.0...v0.1.0
5 changes: 2 additions & 3 deletions prep/src/cmd/clippy.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::process::Command;

use anyhow::{Context, ensure};

use crate::cmd::CargoTargets;
use crate::session::Session;
use crate::tools::cargo;
use crate::ui;

/// Runs Clippy analysis on the given `targets`.
///
/// In `strict` mode warnings are treated as errors.
pub fn run(session: &Session, targets: CargoTargets, strict: bool) -> anyhow::Result<()> {
let mut cmd = Command::new("cargo");
let mut cmd = cargo::new("")?;
let mut cmd = cmd
.current_dir(session.root_dir())
.arg("clippy")
Expand Down
5 changes: 2 additions & 3 deletions prep/src/cmd/format.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::process::Command;

use anyhow::{Context, ensure};

use crate::session::Session;
use crate::tools::cargo;
use crate::ui;

/// Format the workspace
pub fn run(session: &Session, check: bool) -> anyhow::Result<()> {
let mut cmd = Command::new("cargo");
let mut cmd = cargo::new("")?;
let mut cmd = cmd.current_dir(session.root_dir()).arg("fmt").arg("--all");
if check {
cmd = cmd.arg("--check");
Expand Down
1 change: 1 addition & 0 deletions prep/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod cmd;
mod config;
mod session;
mod tools;
mod ui;

use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
Expand Down
3 changes: 3 additions & 0 deletions prep/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use anyhow::{Context, Result, bail};
use cargo_metadata::MetadataCommand;

use crate::config::Config;
use crate::tools::cargo;

const PREP_DIR: &str = ".prep";
const CONFIG_FILE: &str = "prep.toml";
Expand Down Expand Up @@ -44,7 +45,9 @@ impl Session {
let root_dir = match root_dir {
Some(root_dir) => root_dir,
None => {
let cmd = cargo::new("")?;
let metadata = MetadataCommand::new()
.cargo_path(cmd.get_program())
.exec()
.context("failed to fetch Cargo metadata")?;
let workspace_dir = metadata.workspace_root.into_std_path_buf();
Expand Down
92 changes: 92 additions & 0 deletions prep/src/tools/cargo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::collections::HashMap;
use std::io::ErrorKind;
use std::process::Command;
use std::sync::{LazyLock, RwLock};

use anyhow::{Context, Result, bail, ensure};

use crate::tools::rustup;
use crate::ui;

/// Cargo executable name.
const BIN: &str = "cargo";

/// Toolchain version -> Cargo path
static PATHS: LazyLock<RwLock<HashMap<String, String>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));

/// Returns the cargo command.
///
/// Provide an empty `version` to pick the default cargo version.
pub fn new(version: &str) -> Result<Command> {
let paths = PATHS.read().expect("cargo setup lock poisoned");
if let Some(path) = paths.get(version) {
return Ok(Command::new(path));
}
drop(paths);
let mut paths = PATHS.write().expect("cargo setup lock poisoned");
if let Some(path) = paths.get(version) {
return Ok(Command::new(path));
}
let path = set_up(version)?;
let cmd = Command::new(&path);
paths.insert(version.into(), path);
Ok(cmd)
}

/// Ensures that Cargo is installed and ready to use.
pub fn set_up(version: &str) -> Result<String> {
// TODO: Call rustup toolchain install with correct components etc first

let mut cmd = rustup::new()?;
let mut cmd = cmd.arg("which").arg(BIN);
if !version.is_empty() {
cmd = cmd.args(["--toolchain", version]);
}

ui::print_cmd(cmd);

let output = cmd.output().context("failed to run rustup")?;
ensure!(output.status.success(), "rustup failed: {}", output.status);

let path = String::from_utf8(output.stdout).context("rustup output not valid UTF-8")?;
let path = path.trim();

if !verify(path, version)? {
bail!("cargo not found");
}

Ok(path.into())
}

/// Returns `true` if Cargo was found, `false` if no Cargo was found.
///
/// Other versions will return an error.
pub fn verify(path: &str, version: &str) -> Result<bool> {
let mut cmd = Command::new(path);
let cmd = cmd.arg("-V");

ui::print_cmd(cmd);

let output = cmd.output();
if output
.as_ref()
.is_err_and(|e| e.kind() == ErrorKind::NotFound)
{
return Ok(false);
}
let output = output.context("failed to run cargo")?;
ensure!(output.status.success(), "cargo failed: {}", output.status);

let cmd_version = String::from_utf8(output.stdout).context("cargo output not valid UTF-8")?;

let expected = format!("cargo {version}");
if !cmd_version.starts_with(&expected) {
bail!("expected {expected}, got: {cmd_version}");
}

Ok(true)
}
5 changes: 5 additions & 0 deletions prep/src/tools/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

pub mod cargo;
pub mod rustup;
77 changes: 77 additions & 0 deletions prep/src/tools/rustup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::io::ErrorKind;
use std::process::Command;
use std::sync::RwLock;

use anyhow::{Context, Result, bail, ensure};

use crate::ui;

/// Rustup executable name.
const BIN: &str = "rustup";

/// Whether rustup is ready to use.
static READY: RwLock<bool> = RwLock::new(false);

/// Returns the rustup command.
pub fn new() -> Result<Command> {
if !*READY.read().expect("rustup setup lock poisoned") {
let mut ready = READY.write().expect("rustup setup lock poisoned");
if !*ready {
set_up().context("failed to set up rustup")?;
*ready = true;
}
}
Ok(Command::new(BIN))
}

/// Ensures that rustup is installed and ready to use.
pub fn set_up() -> Result<()> {
// Check if rustup is already available
let found = verify()?;
if !found {
ui::print_err(
"\
Prep requires rustup v1 to function.\n\
\n\
There is no automatic setup implemented for it, sorry.\n\
Please go to https://rustup.rs/ and install it manually.\n\
\n\
If you already have rustup installed then this error here is probably a bug.\n\
Please report it at https://github.com/Nevermore/prep\n\
",
);
bail!("rustup not found");
}
Ok(())
}

/// Returns `true` if rustup v1 was found, `false` if no rustup was found.
///
/// Other versions will return an error.
pub fn verify() -> Result<bool> {
let mut cmd = Command::new(BIN);
let cmd = cmd.arg("-V");

ui::print_cmd(cmd);

let output = cmd.output();
if output
.as_ref()
.is_err_and(|e| e.kind() == ErrorKind::NotFound)
{
return Ok(false);
}
let output = output.context("failed to run rustup")?;
ensure!(output.status.success(), "rustup failed: {}", output.status);

let version = String::from_utf8(output.stdout).context("rustup output not valid UTF-8")?;

if !version.starts_with("rustup 1.") {
bail!("expected rustup v1, got: {version}");
}

Ok(true)
}