Skip to content

Commit aa8ddfd

Browse files
authored
Add automatic tool management starting with rustup and cargo. (#24)
* Verifies that `rustup` is installed and gives instructions on how to install it if not. * Verifies that `cargo` is installed and the correct version.
1 parent c97bf5e commit aa8ddfd

8 files changed

Lines changed: 184 additions & 6 deletions

File tree

CHANGELOG.md

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

1213
### Changed
@@ -29,6 +30,7 @@
2930
[#18]: https://github.com/Nevermore/prep/pull/18
3031
[#22]: https://github.com/Nevermore/prep/pull/22
3132
[#23]: https://github.com/Nevermore/prep/pull/23
33+
[#24]: https://github.com/Nevermore/prep/pull/24
3234

3335
[Unreleased]: https://github.com/Nevermore/prep/compare/v0.1.0...HEAD
3436
[0.1.0]: https://github.com/Nevermore/prep/compare/v0.0.0...v0.1.0

prep/src/cmd/clippy.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
// Copyright 2026 the Prep Authors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4-
use std::process::Command;
5-
64
use anyhow::{Context, ensure};
75

86
use crate::cmd::CargoTargets;
97
use crate::session::Session;
8+
use crate::tools::cargo;
109
use crate::ui;
1110

1211
/// Runs Clippy analysis on the given `targets`.
1312
///
1413
/// In `strict` mode warnings are treated as errors.
1514
pub fn run(session: &Session, targets: CargoTargets, strict: bool) -> anyhow::Result<()> {
16-
let mut cmd = Command::new("cargo");
15+
let mut cmd = cargo::new("")?;
1716
let mut cmd = cmd
1817
.current_dir(session.root_dir())
1918
.arg("clippy")

prep/src/cmd/format.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
// Copyright 2026 the Prep Authors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4-
use std::process::Command;
5-
64
use anyhow::{Context, ensure};
75

86
use crate::session::Session;
7+
use crate::tools::cargo;
98
use crate::ui;
109

1110
/// Format the workspace
1211
pub fn run(session: &Session, check: bool) -> anyhow::Result<()> {
13-
let mut cmd = Command::new("cargo");
12+
let mut cmd = cargo::new("")?;
1413
let mut cmd = cmd.current_dir(session.root_dir()).arg("fmt").arg("--all");
1514
if check {
1615
cmd = cmd.arg("--check");

prep/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
mod cmd;
77
mod config;
88
mod session;
9+
mod tools;
910
mod ui;
1011

1112
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};

prep/src/session.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use anyhow::{Context, Result, bail};
1010
use cargo_metadata::MetadataCommand;
1111

1212
use crate::config::Config;
13+
use crate::tools::cargo;
1314

1415
const PREP_DIR: &str = ".prep";
1516
const CONFIG_FILE: &str = "prep.toml";
@@ -44,7 +45,9 @@ impl Session {
4445
let root_dir = match root_dir {
4546
Some(root_dir) => root_dir,
4647
None => {
48+
let cmd = cargo::new("")?;
4749
let metadata = MetadataCommand::new()
50+
.cargo_path(cmd.get_program())
4851
.exec()
4952
.context("failed to fetch Cargo metadata")?;
5053
let workspace_dir = metadata.workspace_root.into_std_path_buf();

prep/src/tools/cargo.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2026 the Prep Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use std::collections::HashMap;
5+
use std::io::ErrorKind;
6+
use std::process::Command;
7+
use std::sync::{LazyLock, RwLock};
8+
9+
use anyhow::{Context, Result, bail, ensure};
10+
11+
use crate::tools::rustup;
12+
use crate::ui;
13+
14+
/// Cargo executable name.
15+
const BIN: &str = "cargo";
16+
17+
/// Toolchain version -> Cargo path
18+
static PATHS: LazyLock<RwLock<HashMap<String, String>>> =
19+
LazyLock::new(|| RwLock::new(HashMap::new()));
20+
21+
/// Returns the cargo command.
22+
///
23+
/// Provide an empty `version` to pick the default cargo version.
24+
pub fn new(version: &str) -> Result<Command> {
25+
let paths = PATHS.read().expect("cargo setup lock poisoned");
26+
if let Some(path) = paths.get(version) {
27+
return Ok(Command::new(path));
28+
}
29+
drop(paths);
30+
let mut paths = PATHS.write().expect("cargo setup lock poisoned");
31+
if let Some(path) = paths.get(version) {
32+
return Ok(Command::new(path));
33+
}
34+
let path = set_up(version)?;
35+
let cmd = Command::new(&path);
36+
paths.insert(version.into(), path);
37+
Ok(cmd)
38+
}
39+
40+
/// Ensures that Cargo is installed and ready to use.
41+
pub fn set_up(version: &str) -> Result<String> {
42+
// TODO: Call rustup toolchain install with correct components etc first
43+
44+
let mut cmd = rustup::new()?;
45+
let mut cmd = cmd.arg("which").arg(BIN);
46+
if !version.is_empty() {
47+
cmd = cmd.args(["--toolchain", version]);
48+
}
49+
50+
ui::print_cmd(cmd);
51+
52+
let output = cmd.output().context("failed to run rustup")?;
53+
ensure!(output.status.success(), "rustup failed: {}", output.status);
54+
55+
let path = String::from_utf8(output.stdout).context("rustup output not valid UTF-8")?;
56+
let path = path.trim();
57+
58+
if !verify(path, version)? {
59+
bail!("cargo not found");
60+
}
61+
62+
Ok(path.into())
63+
}
64+
65+
/// Returns `true` if Cargo was found, `false` if no Cargo was found.
66+
///
67+
/// Other versions will return an error.
68+
pub fn verify(path: &str, version: &str) -> Result<bool> {
69+
let mut cmd = Command::new(path);
70+
let cmd = cmd.arg("-V");
71+
72+
ui::print_cmd(cmd);
73+
74+
let output = cmd.output();
75+
if output
76+
.as_ref()
77+
.is_err_and(|e| e.kind() == ErrorKind::NotFound)
78+
{
79+
return Ok(false);
80+
}
81+
let output = output.context("failed to run cargo")?;
82+
ensure!(output.status.success(), "cargo failed: {}", output.status);
83+
84+
let cmd_version = String::from_utf8(output.stdout).context("cargo output not valid UTF-8")?;
85+
86+
let expected = format!("cargo {version}");
87+
if !cmd_version.starts_with(&expected) {
88+
bail!("expected {expected}, got: {cmd_version}");
89+
}
90+
91+
Ok(true)
92+
}

prep/src/tools/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright 2026 the Prep Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
pub mod cargo;
5+
pub mod rustup;

prep/src/tools/rustup.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2026 the Prep Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use std::io::ErrorKind;
5+
use std::process::Command;
6+
use std::sync::RwLock;
7+
8+
use anyhow::{Context, Result, bail, ensure};
9+
10+
use crate::ui;
11+
12+
/// Rustup executable name.
13+
const BIN: &str = "rustup";
14+
15+
/// Whether rustup is ready to use.
16+
static READY: RwLock<bool> = RwLock::new(false);
17+
18+
/// Returns the rustup command.
19+
pub fn new() -> Result<Command> {
20+
if !*READY.read().expect("rustup setup lock poisoned") {
21+
let mut ready = READY.write().expect("rustup setup lock poisoned");
22+
if !*ready {
23+
set_up().context("failed to set up rustup")?;
24+
*ready = true;
25+
}
26+
}
27+
Ok(Command::new(BIN))
28+
}
29+
30+
/// Ensures that rustup is installed and ready to use.
31+
pub fn set_up() -> Result<()> {
32+
// Check if rustup is already available
33+
let found = verify()?;
34+
if !found {
35+
ui::print_err(
36+
"\
37+
Prep requires rustup v1 to function.\n\
38+
\n\
39+
There is no automatic setup implemented for it, sorry.\n\
40+
Please go to https://rustup.rs/ and install it manually.\n\
41+
\n\
42+
If you already have rustup installed then this error here is probably a bug.\n\
43+
Please report it at https://github.com/Nevermore/prep\n\
44+
",
45+
);
46+
bail!("rustup not found");
47+
}
48+
Ok(())
49+
}
50+
51+
/// Returns `true` if rustup v1 was found, `false` if no rustup was found.
52+
///
53+
/// Other versions will return an error.
54+
pub fn verify() -> Result<bool> {
55+
let mut cmd = Command::new(BIN);
56+
let cmd = cmd.arg("-V");
57+
58+
ui::print_cmd(cmd);
59+
60+
let output = cmd.output();
61+
if output
62+
.as_ref()
63+
.is_err_and(|e| e.kind() == ErrorKind::NotFound)
64+
{
65+
return Ok(false);
66+
}
67+
let output = output.context("failed to run rustup")?;
68+
ensure!(output.status.success(), "rustup failed: {}", output.status);
69+
70+
let version = String::from_utf8(output.stdout).context("rustup output not valid UTF-8")?;
71+
72+
if !version.starts_with("rustup 1.") {
73+
bail!("expected rustup v1, got: {version}");
74+
}
75+
76+
Ok(true)
77+
}

0 commit comments

Comments
 (0)