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
61 changes: 61 additions & 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 @@ -16,3 +16,4 @@ rust.missing_docs = "warn"
[workspace.dependencies]
anyhow = "1.0.100"
clap = "4.5.56"
time = "0.3.46"
1 change: 1 addition & 0 deletions prep/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ workspace = true
[dependencies]
anyhow.workspace = true
clap = { workspace = true, features = ["derive"] }
time.workspace = true
61 changes: 61 additions & 0 deletions prep/src/cmd/copyright.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2026 the Prep Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::process::Command;

use anyhow::{Context, bail, ensure};
use time::UtcDateTime;

use crate::ui;
use crate::ui::style::{ERROR, HEADER, LITERAL, NOTE};

// TODO: Allow configuring the regex
// TODO: Allow excluding files from the check
// TODO: ALlow configuring the project name
// TODO: Allow configuring the license (or fetch it from Cargo.toml per-package?)

const REGEX: &str = r#"^// Copyright (19|20)[\d]{2} (.+ and )?the Prep Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n"#;

/// Verify copyright headers.
pub fn run() -> anyhow::Result<()> {
let mut cmd = Command::new("rg");
let cmd = cmd
.arg(REGEX)
.arg("--files-without-match")
.arg("--multiline")
.args(["-g", "*.rs"])
.arg(".");

ui::print_cmd(cmd);

let output = cmd.output().context("failed to run ripgrep")?;

// ripgrep exits with code 1 in case of no matches, code 2 in case of error
ensure!(
output.status.success() || output.status.code().is_some_and(|code| code == 1),
"ripgrep failed: {}",
output.status
);

if !output.stdout.is_empty() {
print_missing(String::from_utf8(output.stdout).unwrap());
bail!("failed copyright header verification");
}

let h = HEADER;
eprintln!(" {h}Verified{h:#} all source files have correct copyright headers.");

Ok(())
}

fn print_missing(msg: String) {
let (e, l, n) = (ERROR, LITERAL, NOTE);
let year = UtcDateTime::now().year();

eprintln!("{e}The following files lack the correct copyright header:{e:#}");
eprintln!("{l}{msg}{l:#}");
eprintln!("{n}Please add the following header:{n:#}\n");
eprintln!("// Copyright {year} the Prep Authors");
eprintln!("// SPDX-License-Identifier: Apache-2.0 OR MIT");
eprintln!("\n... rest of the file ...\n");
}
1 change: 1 addition & 0 deletions prep/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use clap::ValueEnum;

pub mod ci;
pub mod clippy;
pub mod copyright;
pub mod format;

/// Cargo targets.
Expand Down
3 changes: 3 additions & 0 deletions prep/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ enum Commands {
#[arg(short, long)]
strict: bool,
},
#[command()]
Copyright,
#[command(alias = "fmt")]
Format {
#[arg(short, long)]
Expand All @@ -58,6 +60,7 @@ fn main() -> anyhow::Result<()> {
no_fail_fast,
} => cmd::ci::run(extended, !no_fail_fast),
Commands::Clippy { targets, strict } => cmd::clippy::run(targets, strict),
Commands::Copyright => cmd::copyright::run(),
Commands::Format { check } => cmd::format::run(check),
}
}
101 changes: 60 additions & 41 deletions prep/src/ui/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use clap::Command;
use clap::builder::StyledStr;

use crate::ui;
use crate::ui::style::{HEADER, LITERAL, PLACEHOLDER};

/// Sets our custom help messages.
pub fn set(cmd: Command) -> Command {
Expand All @@ -18,6 +18,8 @@ pub fn set(cmd: Command) -> Command {
scmd.override_help(format_msg())
} else if name == "clippy" {
scmd.override_help(clippy_msg())
} else if name == "copyright" {
scmd.override_help(copyright_msg())
} else {
panic!("Sub-command '{name}' help message is not implemented");
}
Expand All @@ -26,22 +28,23 @@ pub fn set(cmd: Command) -> Command {

/// Returns the main help message.
pub fn root_msg() -> StyledStr {
let (gb, cb, bb) = ui::styles();
let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER);
let help = format!(
"\
Prepare Rust projects for greatness.

{gb}Usage:{gb:#} {cb}prep{cb:#} {bb}[command] [options]{bb:#}
{h}Usage:{h:#} {l}prep{l:#} {p}[command] [options]{p:#}

{gb}Commands:{gb:#}
{cb} ci {cb:#}Verify for CI.
{cb}clp clippy {cb:#}Analyze with Clippy.
{cb}fmt format {cb:#}Format with rustfmt.
{cb} help {cb:#}Print help for the provided command.
{h}Commands:{h:#}
{l} ci {l:#}Verify for CI.
{l}clp clippy {l:#}Analyze with Clippy.
{l} copyright {l:#}Verify copyright headers.
{l}fmt format {l:#}Format with rustfmt.
{l} help {l:#}Print help for the provided command.

{gb}Options:{gb:#}
{cb}-h --help {cb:#}Print help for the provided command.
{cb}-V --version {cb:#}Print version information.
{h}Options:{h:#}
{l}-h --help {l:#}Print help for the provided command.
{l}-V --version {l:#}Print version information.
"
);

Expand All @@ -50,64 +53,80 @@ Prepare Rust projects for greatness.

/// Returns the `ci` help message.
fn ci_msg() -> StyledStr {
let (gb, cb, bb) = ui::styles();

let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER);
let help = format!(
"\
Verify the Rust workspace for CI.

{gb}Usage:{gb:#} {cb}prep ci{cb:#} {bb}[options]{bb:#}
{h}Usage:{h:#} {l}prep ci{l:#} {p}[options]{p:#}

{gb}Options:{gb:#}
{cb}-e --extended {cb:#}Run the extended verification suite.
···· ······Good idea for actual CI, rarely useful for local prep.
{cb}-n --no-fail-fast {cb:#}Keep going when encountering an error.
{cb}-h --help {cb:#}Print this help message.
{h}Options:{h:#}
{l}-e --extended {l:#}Run the extended verification suite.
··· ·····Good idea for actual CI, rarely useful for local prep.
{l}-n --no-fail-fast {l:#}Keep going when encountering an error.
{l}-h --help {l:#}Print this help message.
"
)
.replace("·", "");

StyledStr::from(help)
}

/// Returns the `format` help message.
fn format_msg() -> StyledStr {
let (gb, cb, bb) = ui::styles();
/// Returns the `clippy` help message.
fn clippy_msg() -> StyledStr {
let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER);
let help = format!(
"\
Analyze the Rust workspace with Clippy.

{h}Usage:{h:#} {l}prep clp{l:#} {p}[options]{p:#}
··· ····· {l}prep clippy{l:#} {p}[options]{p:#}

{h}Options:{h:#}
{l}-c --crates <val> {l:#}Target specified crates. Possible values:
··· ·····{p}main{p:#} -> Binaries and the main library. (default)
··· ·····{p}aux{p:#} -> Examples, tests, and benches.
··· ·····{p}all{p:#} -> All of the above.
{l}-s --strict {l:#}Treat warnings as errors.
{l}-h --help {l:#}Print this help message.
"
)
.replace("·", "");

StyledStr::from(help)
}

/// Returns the `copyright` help message.
fn copyright_msg() -> StyledStr {
let (h, l) = (HEADER, LITERAL);
let help = format!(
"\
Format the Rust workspace with rustfmt.
Verify that all Rust source files have the correct copyright header.

{gb}Usage:{gb:#} {cb}prep fmt{cb:#} {bb}[options]{bb:#}
···· ······ {cb}prep format{cb:#} {bb}[options]{bb:#}
{h}Usage:{h:#} {l}prep copyright{l:#}

{gb}Options:{gb:#}
{cb}-c --check {cb:#}Verify that the workspace is already formatted.
{cb}-h --help {cb:#}Print this help message.
{h}Options:{h:#}
{l}-h --help {l:#}Print this help message.
"
)
.replace("·", "");

StyledStr::from(help)
}

/// Returns the `clippy` help message.
fn clippy_msg() -> StyledStr {
let (gb, cb, bb) = ui::styles();
/// Returns the `format` help message.
fn format_msg() -> StyledStr {
let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER);
let help = format!(
"\
Analyze the Rust workspace with Clippy.
Format the Rust workspace with rustfmt.

{gb}Usage:{gb:#} {cb}prep clp{cb:#} {bb}[options]{bb:#}
···· ······ {cb}prep clippy{cb:#} {bb}[options]{bb:#}
{h}Usage:{h:#} {l}prep fmt{l:#} {p}[options]{p:#}
··· ····· {l}prep format{l:#} {p}[options]{p:#}

{gb}Options:{gb:#}
{cb}-c --crates <val> {cb:#}Target specified crates. Possible values:
···· ······{bb}main{bb:#} -> Binaries and the main library. (default)
···· ······{bb}aux{bb:#} -> Examples, tests, and benches.
···· ······{bb}all{bb:#} -> All of the above.
{cb}-s --strict {cb:#}Treat warnings as errors.
{cb}-h --help {cb:#}Print this help message.
{h}Options:{h:#}
{l}-c --check {l:#}Verify that the workspace is already formatted.
{l}-h --help {l:#}Print this help message.
"
)
.replace("·", "");
Expand Down
Loading