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
5 changes: 5 additions & 0 deletions .prep/prep.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[project]
name = "Prep"
license = "Apache-2.0 OR MIT"

[tools]
rustup = "=1"
rust = "=1.93"
ripgrep = "=14.1.1"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Added

* `tools` command for tool management. ([#27] by [@xStrom])
* `--strict` option to `clippy`, `copyright`, and `format` commands to use locked tool versions. ([#27] by [@xStrom])

## [0.2.0] - 2026-02-07

### Added
Expand Down Expand Up @@ -35,6 +38,7 @@
[#22]: https://github.com/Nevermore/prep/pull/22
[#23]: https://github.com/Nevermore/prep/pull/23
[#24]: https://github.com/Nevermore/prep/pull/24
[#27]: https://github.com/Nevermore/prep/pull/27

[Unreleased]: https://github.com/Nevermore/prep/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/Nevermore/prep/compare/v0.1.0...v0.2.0
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ anyhow = "1.0.101"
cargo_metadata = "0.23.1"
clap = "4.5.57"
regex = "1.12.3"
semver = "1.0.27"
serde = "1.0.228"
time = "0.3.47"
toml = "0.9.11"
1 change: 1 addition & 0 deletions prep/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ anyhow.workspace = true
cargo_metadata.workspace = true
clap = { workspace = true, features = ["derive"] }
regex.workspace = true
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
time.workspace = true
toml.workspace = true
12 changes: 6 additions & 6 deletions prep/src/cmd/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::session::Session;
/// Can be ran in `extended` mode for more thorough checks.
///
/// Set `fail_fast` to `false` to run the checks to the end regardless of failure.
pub fn run(session: &Session, extended: bool, fail_fast: bool) -> anyhow::Result<()> {
pub fn run(session: &mut Session, extended: bool, fail_fast: bool) -> anyhow::Result<()> {
let mut errs: Vec<anyhow::Error> = Vec::new();
let mut step = |f: &dyn Fn() -> anyhow::Result<()>| -> anyhow::Result<()> {
let mut step = |f: &mut dyn FnMut() -> anyhow::Result<()>| -> anyhow::Result<()> {
if let Err(e) = f() {
if fail_fast {
return Err(e);
Expand All @@ -22,16 +22,16 @@ pub fn run(session: &Session, extended: bool, fail_fast: bool) -> anyhow::Result
};

//step(&|| copyright::run(session))?;
step(&|| format::run(session, true))?;
step(&mut || format::run(session, true, true))?;

if extended {
// We need to avoid --all-targets because it will unify dev and regular dep features.
step(&|| clippy::run(session, CargoTargets::Main, true))?;
step(&|| clippy::run(session, CargoTargets::Auxiliary, true))?;
step(&mut || clippy::run(session, true, CargoTargets::Main))?;
step(&mut || clippy::run(session, true, CargoTargets::Auxiliary))?;
} else {
// Slightly faster due to shared build cache,
// but will miss unified feature bugs.
step(&|| clippy::run(session, CargoTargets::All, true))?;
step(&mut || clippy::run(session, true, CargoTargets::All))?;
}

if errs.is_empty() {
Expand Down
19 changes: 15 additions & 4 deletions prep/src/cmd/clippy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ use anyhow::{Context, ensure};

use crate::cmd::CargoTargets;
use crate::session::Session;
use crate::tools::cargo;
use crate::tools::cargo::{Cargo, CargoDeps};
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 = cargo::new("")?;
/// In `strict` mode warnings are treated as errors and Cargo version is locked.
pub fn run(session: &mut Session, strict: bool, targets: CargoTargets) -> anyhow::Result<()> {
let mut cmd = if strict {
let tools_cfg = session.config().tools();
let rustup_ver_req = tools_cfg.rustup().clone();
let ver_req = tools_cfg.rust().clone();
let toolset = session.toolset();
let deps = CargoDeps::new(rustup_ver_req);
toolset.get::<Cargo>(&deps, &ver_req)?
} else {
let toolset = session.toolset();
let deps = CargoDeps::new(None);
toolset.get::<Cargo>(&deps, None)?
};
let mut cmd = cmd
.current_dir(session.root_dir())
.arg("clippy")
Expand Down
5 changes: 4 additions & 1 deletion prep/src/cmd/copyright.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ use crate::ui::style::{ERROR, HEADER, LITERAL, NOTE};
// TODO: Allow excluding files from the check

/// Verify copyright headers.
pub fn run(session: &Session) -> anyhow::Result<()> {
///
/// In `strict` mode ripgrep version is locked.
pub fn run(session: &mut Session, _strict: bool) -> anyhow::Result<()> {
let config = session.config();
let project = config.project();
let header_regex = header_regex(project.name(), project.license());

// TODO: Strict mode for ripgrep.
let mut cmd = Command::new("rg");
let cmd = cmd
.current_dir(session.root_dir())
Expand Down
21 changes: 17 additions & 4 deletions prep/src/cmd/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@
use anyhow::{Context, ensure};

use crate::session::Session;
use crate::tools::cargo;
use crate::tools::cargo::{Cargo, CargoDeps};
use crate::ui;

/// Format the workspace
pub fn run(session: &Session, check: bool) -> anyhow::Result<()> {
let mut cmd = cargo::new("")?;
/// Format the workspace.
///
/// In `strict` mode Cargo version is locked.
pub fn run(session: &mut Session, strict: bool, check: bool) -> anyhow::Result<()> {
let mut cmd = if strict {
let tools_cfg = session.config().tools();
let rustup_ver_req = tools_cfg.rustup().clone();
let ver_req = tools_cfg.rust().clone();
let toolset = session.toolset();
let deps = CargoDeps::new(rustup_ver_req);
toolset.get::<Cargo>(&deps, &ver_req)?
} else {
let toolset = session.toolset();
let deps = CargoDeps::new(None);
toolset.get::<Cargo>(&deps, None)?
};
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/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod clippy;
pub mod copyright;
pub mod format;
pub mod init;
pub mod tools;

/// Cargo targets.
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
Expand Down
64 changes: 64 additions & 0 deletions prep/src/cmd/tools/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::session::Session;
use crate::tools::cargo::Cargo;
use crate::tools::rustup::Rustup;
use crate::ui::style::TABLE_HEADER;

const MISSING: &str = "None";

/// List information on all the tools in the toolset.
pub fn run(session: &mut Session) -> anyhow::Result<()> {
let tools = session.config().tools();

let rustup_locked = format!("{}", tools.rustup());
let rust_locked = format!("{}", tools.rust());
let rg_locked = format!("{}", tools.ripgrep());

let toolset = session.toolset();

let rustup_global = toolset
.default_version::<Rustup>()?
.map(|v| format!("{v}"))
.unwrap_or_else(|| MISSING.into());
let rust_global = toolset
.default_version::<Cargo>()?
.map(|v| format!("{v}"))
.unwrap_or_else(|| MISSING.into());
let rg_global = String::from("Who knows");

fn cell(s: &str, len: usize) -> String {
let mut s = String::from(s);
s.push_str(&" ".repeat(len.saturating_sub(s.len())));
s
}

const NLEN: usize = 7;
const LLEN: usize = 16;
const GLEN: usize = 15;

let h = TABLE_HEADER;
let info = format!(
"\
{h}Name{h:#} {h}Required version{h:#} {h}Default version{h:#}
···{}·········· ···{}··················· ···{}··················
···{}·········· ···{}··················· ···{}··················
···{}·········· ···{}··················· ···{}··················
",
cell("Rustup", NLEN),
cell(rustup_locked.trim_start_matches('='), LLEN),
cell(&rustup_global, GLEN),
cell("Rust", NLEN),
cell(rust_locked.trim_start_matches('='), LLEN),
cell(&rust_global, GLEN),
cell("Ripgrep", NLEN),
cell(rg_locked.trim_start_matches('='), LLEN),
cell(&rg_global, GLEN),
)
.replace("·", "");

eprint!("{}", info);

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

pub mod list;
91 changes: 85 additions & 6 deletions prep/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use semver::VersionReq;
use serde::{Deserialize, Serialize};

/// Prep project configuration.
/// Prep configuration.
#[derive(Serialize, Deserialize)]
pub struct Config {
/// The project configuration.
/// Project configuration.
#[serde(default = "Project::new")]
project: Project,
/// Tools configuration.
#[serde(default = "Tools::new")]
tools: Tools,
}

/// Project configuration.
#[derive(Serialize, Deserialize)]
pub struct Project {
/// The project name.
/// Project name.
#[serde(default = "name_default")]
name: String,
/// The project License SPDX identifier.
/// Project License SPDX identifier.
#[serde(default = "license_default")]
license: String,
}

/// Tools configuration.
#[derive(Serialize, Deserialize)]
pub struct Tools {
/// Rustup configuration.
#[serde(default = "rustup_default")]
rustup: VersionReq,
/// Stable Rust toolchain configuration.
#[serde(default = "rust_default")]
rust: VersionReq,
/// Ripgrep configuration.
#[serde(default = "ripgrep_default")]
ripgrep: VersionReq,
}

impl Config {
/// Creates a new [`Config`] with default values.
pub fn new() -> Self {
Self {
project: Project::new(),
tools: Tools::new(),
}
}

/// Returns the project configuration.
pub fn project(&self) -> &Project {
&self.project
}

/// Returns the tools configuration.
pub fn tools(&self) -> &Tools {
&self.tools
}
}

impl Project {
/// Creates a new [`Project`] with default values.
pub fn new() -> Self {
Self {
name: "Untitled".into(),
license: "Apache-2.0 OR MIT".into(),
name: name_default(),
license: license_default(),
}
}

Expand All @@ -51,3 +79,54 @@ impl Project {
&self.license
}
}

impl Tools {
/// Creates a new [`Tools`] with default values.
pub fn new() -> Self {
Self {
rustup: rustup_default(),
rust: rust_default(),
ripgrep: ripgrep_default(),
}
}

/// Returns the configured Rustup version.
pub fn rustup(&self) -> &VersionReq {
&self.rustup
}

/// Returns the configured stable Rust toolchain version.
pub fn rust(&self) -> &VersionReq {
&self.rust
}

/// Returns the configured ripgrep version.
pub fn ripgrep(&self) -> &VersionReq {
&self.ripgrep
}
}

/// Returns the default project name.
fn name_default() -> String {
"Untitled".into()
}

/// Returns the default project license.
fn license_default() -> String {
"Apache-2.0 OR MIT".into()
}

/// Returns the default Rustup version.
fn rustup_default() -> VersionReq {
VersionReq::parse("=1").expect("default rustup version parsing failed")
}

/// Returns the default Rust version.
fn rust_default() -> VersionReq {
VersionReq::parse("=1.93").expect("default rust version parsing failed")
}

/// Returns the default Ripgrep version.
fn ripgrep_default() -> VersionReq {
VersionReq::parse("=14.1.1").expect("default ripgrep version parsing failed")
}
Loading