use std::path::{Path, PathBuf};
use clap::{Args, Parser, Subcommand};
use crate::error::{HoldError, Result};
#[cfg(test)]
mod tests;
#[derive(Parser)]
#[command(
name = "cargo-hold",
bin_name = "cargo-hold",
author,
version,
about = "A CI tool to ensure Cargo's incremental compilation is reliable",
long_about = None,
propagate_version = true
)]
pub struct Cli {
#[command(flatten)]
global_opts: GlobalOpts,
#[command(subcommand)]
command: Commands,
}
#[derive(Parser)]
pub struct GlobalOpts {
#[arg(
long,
global = true,
default_value = "target",
env = "CARGO_HOLD_TARGET_DIR"
)]
target_dir: PathBuf,
#[arg(long, global = true, env = "CARGO_HOLD_METADATA_PATH")]
metadata_path: Option<PathBuf>,
#[arg(short, long, global = true, action = clap::ArgAction::Count, env = "CARGO_HOLD_VERBOSE")]
verbose: u8,
#[arg(
short,
long,
global = true,
conflicts_with = "verbose",
env = "CARGO_HOLD_QUIET"
)]
quiet: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct GcArgs {
#[arg(long, env = "CARGO_HOLD_MAX_TARGET_SIZE")]
max_target_size: Option<String>,
#[arg(
long,
value_delimiter = ',',
env = "CARGO_HOLD_PRESERVE_CARGO_BINARIES"
)]
preserve_cargo_binaries: Vec<String>,
}
impl GcArgs {
pub fn new(max_target_size: Option<String>, preserve_cargo_binaries: Vec<String>) -> Self {
Self {
max_target_size,
preserve_cargo_binaries,
}
}
pub fn max_target_size(&self) -> Option<&str> {
self.max_target_size.as_deref()
}
pub fn preserve_cargo_binaries(&self) -> &[String] {
&self.preserve_cargo_binaries
}
}
impl GlobalOpts {
pub fn builder() -> GlobalOptsBuilder {
GlobalOptsBuilder::default()
}
pub fn get_metadata_path(&self) -> PathBuf {
let path = self
.metadata_path()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| self.target_dir().join("cargo-hold.metadata"));
normalize_path(path)
}
pub fn get_target_dir(&self) -> PathBuf {
normalize_path(self.target_dir())
}
pub fn target_dir(&self) -> &Path {
&self.target_dir
}
pub fn metadata_path(&self) -> Option<&Path> {
self.metadata_path.as_deref()
}
pub fn verbose(&self) -> u8 {
self.verbose
}
pub fn quiet(&self) -> bool {
self.quiet
}
}
#[derive(Default)]
pub struct GlobalOptsBuilder {
target_dir: Option<PathBuf>,
metadata_path: Option<PathBuf>,
verbose: u8,
quiet: bool,
}
impl GlobalOptsBuilder {
pub fn target_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.target_dir = Some(dir.into());
self
}
pub fn metadata_path(mut self, path: Option<impl Into<PathBuf>>) -> Self {
self.metadata_path = path.map(|p| p.into());
self
}
pub fn verbose(mut self, level: u8) -> Self {
self.verbose = level;
self
}
pub fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
pub fn build(self) -> GlobalOpts {
GlobalOpts {
target_dir: self.target_dir.unwrap_or_else(|| PathBuf::from("target")),
metadata_path: self.metadata_path,
verbose: self.verbose,
quiet: self.quiet,
}
}
}
impl Cli {
pub fn global_opts(&self) -> &GlobalOpts {
&self.global_opts
}
pub fn command(&self) -> &Commands {
&self.command
}
pub fn builder() -> CliBuilder {
CliBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct CliBuilder {
target_dir: Option<PathBuf>,
metadata_path: Option<PathBuf>,
verbose: u8,
quiet: bool,
command: Option<Commands>,
}
impl CliBuilder {
pub fn target_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.target_dir = Some(dir.into());
self
}
pub fn metadata_path(mut self, path: impl Into<PathBuf>) -> Self {
self.metadata_path = Some(path.into());
self
}
pub fn verbose(mut self, level: u8) -> Self {
self.verbose = level;
self
}
pub fn quiet(mut self, enabled: bool) -> Self {
self.quiet = enabled;
self
}
pub fn command(mut self, command: Commands) -> Self {
self.command = Some(command);
self
}
pub fn build(self) -> Result<Cli> {
let command = self
.command
.ok_or(HoldError::ConfigError("Command is required".to_string()))?;
Ok(Cli {
global_opts: GlobalOpts::builder()
.target_dir(self.target_dir.unwrap_or_else(|| PathBuf::from("target")))
.metadata_path(self.metadata_path)
.verbose(self.verbose)
.quiet(self.quiet)
.build(),
command,
})
}
}
fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
let absolute = if path.is_relative() {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(path)
} else {
path.to_path_buf()
};
let mut components = Vec::new();
for component in absolute.components() {
use std::path::Component;
match component {
Component::ParentDir => {
if let Some(last) = components.last()
&& !matches!(last, Component::ParentDir)
{
components.pop();
continue;
}
components.push(component);
}
Component::CurDir => {
continue;
}
_ => components.push(component),
}
}
let mut result = PathBuf::new();
for component in components {
result.push(component);
}
result
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Anchor,
Salvage,
Stow,
Bilge,
Heave {
#[command(flatten)]
gc: GcArgs,
#[arg(long, env = "CARGO_HOLD_DRY_RUN")]
dry_run: bool,
#[arg(long, env = "CARGO_HOLD_DEBUG")]
debug: bool,
#[arg(long, default_value = "7", env = "CARGO_HOLD_AGE_THRESHOLD_DAYS")]
age_threshold_days: u32,
#[arg(long, default_value_t = true, env = "CARGO_HOLD_AUTO_MAX_TARGET_SIZE")]
auto_max_target_size: bool,
},
Voyage {
#[command(flatten)]
gc: GcArgs,
#[arg(long, env = "CARGO_HOLD_GC_DRY_RUN")]
gc_dry_run: bool,
#[arg(long, env = "CARGO_HOLD_GC_DEBUG")]
gc_debug: bool,
#[arg(long, default_value = "7", env = "CARGO_HOLD_GC_AGE_THRESHOLD_DAYS")]
gc_age_threshold_days: u32,
#[arg(long, default_value_t = true, env = "CARGO_HOLD_AUTO_MAX_TARGET_SIZE")]
gc_auto_max_target_size: bool,
},
}
impl Cli {
pub fn parse_args() -> Self {
let args: Vec<String> = std::env::args().collect();
if args.len() >= 2 && args[1] == "hold" {
let mut new_args = vec![args[0].clone()]; new_args.extend_from_slice(&args[2..]); return Self::parse_from(new_args);
}
Self::parse()
}
}