pub mod debug;
pub mod fmt;
pub mod lint;
pub mod run;
pub mod trace;
pub mod transpose;
use crate::{log, ErrToStr};
use cargo_metadata::{Dependency, Metadata, MetadataCommand, Package, Resolve};
use std::{fs::canonicalize, path::Path};
#[derive(Debug, clap::Parser)]
#[command(author, version, about, long_about = None)]
pub struct Command {
#[clap(subcommand)]
subcommand: Option<SubCommand>,
#[clap(flatten)]
global: GlobalArgs,
}
#[derive(Debug, clap::Parser)]
pub struct GlobalArgs {
#[clap(long, short, global = true)]
quiet: bool,
#[cfg(feature = "logging")]
#[clap(long = "log", global = true, default_value = "info", ignore_case = true)]
level: ::log::LevelFilter,
#[cfg(not(feature = "logging"))]
#[clap(long = "log", global = true, default_value = "info", ignore_case = true)]
level: String,
#[clap(long, global = true, default_value_t = false)]
color: bool,
#[clap(long, global = true, verbatim_doc_comment)]
exit_code_zero: bool,
#[clap(long, global = true, value_enum, verbatim_doc_comment, default_value_t = FixHint::On)]
fix_hint: FixHint,
}
#[derive(Debug, clap::Subcommand)]
enum SubCommand {
Trace(trace::TraceCmd),
Lint(lint::LintCmd),
#[clap(alias = "fmt", alias = "f")]
Format(fmt::FormatCmd),
Run(run::RunCmd),
#[clap(hide = true)]
Transpose(transpose::TransposeCmd),
Debug(debug::DebugCmd),
}
#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
pub enum FixHint {
On,
Off,
}
impl Command {
pub fn run(&self) -> Result<(), String> {
self.global.setup_logging();
match self.subcommand.as_ref() {
Some(SubCommand::Transpose(cmd)) => cmd.run(&self.global),
Some(SubCommand::Trace(cmd)) => {
cmd.run(&self.global);
Ok(())
},
Some(SubCommand::Lint(cmd)) => cmd.run(&self.global),
Some(SubCommand::Format(cmd)) => {
cmd.run(&self.global);
Ok(())
},
Some(SubCommand::Run(cmd)) => {
cmd.run(&self.global);
Ok(())
},
Some(SubCommand::Debug(cmd)) => {
cmd.run(&self.global);
Ok(())
},
None => {
run::RunCmd::default().run(&self.global);
Ok(())
},
}
}
}
impl GlobalArgs {
pub fn setup_logging(&self) {
#[cfg(feature = "logging")]
if self.quiet {
::log::set_max_level(::log::LevelFilter::Error);
} else {
::log::set_max_level(self.level);
}
}
pub fn warn_unstable(&self) {
log::warn!("Unstable feature - do not rely on this!");
}
pub fn show_hints(&self) -> bool {
match self.fix_hint {
FixHint::On => true,
FixHint::Off => false,
}
}
pub fn error_code(&self) -> i32 {
if self.exit_code_zero {
0
} else {
1
}
}
pub fn error_code_cfg_parsing() -> i32 {
101
}
pub fn red(&self, s: &str) -> String {
if !self.color {
s.to_string()
} else {
format!("\x1b[31m{s}\x1b[0m")
}
}
pub fn yellow(&self, s: &str) -> String {
if !self.color {
s.to_string()
} else {
format!("\x1b[33m{s}\x1b[0m")
}
}
pub fn green(&self, s: &str) -> String {
if !self.color {
s.to_string()
} else {
format!("\x1b[32m{s}\x1b[0m")
}
}
pub fn bold(&self, s: &str) -> String {
if !self.color {
s.to_string()
} else {
format!("\x1b[1m{s}\x1b[0m")
}
}
}
#[derive(Debug, Clone, clap::Parser, PartialEq)]
pub struct CargoArgs {
#[arg(long, global = true)]
pub manifest_path: Option<std::path::PathBuf>,
#[clap(long, global = true)]
pub workspace: bool,
#[clap(long, global = true)]
pub offline: bool,
#[clap(long, global = true)]
pub locked: bool,
#[clap(long, global = true)]
pub all_features: bool,
}
impl CargoArgs {
pub fn with_workspace(mut self, workspace: bool) -> Self {
self.workspace = workspace;
self
}
pub fn load_metadata(&self) -> Result<Metadata, String> {
let err = match self.load_metadata_unsorted() {
Ok(meta) => return Ok(meta),
Err(err) => err,
};
if check_for_locked_error(&err) {
Err("\nThe Cargo.lock file needs to be updated first since --locked is present.\n"
.to_string())
} else {
Err(err)
}
}
fn load_metadata_unsorted(&self) -> Result<Metadata, String> {
let mut cmd = MetadataCommand::new();
if let Some(ref manifest_path) = self.manifest_path {
let manifest_path = if manifest_path.is_dir() {
manifest_path.join("Cargo.toml")
} else {
manifest_path.clone()
};
cmd.manifest_path(&manifest_path);
}
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
if self.workspace {
cmd.no_deps();
}
if self.offline {
cmd.other_options(vec!["--offline".to_string()]);
}
if self.locked {
cmd.other_options(vec!["--locked".to_string()]);
}
cmd.exec().map_err(|e| format!("Failed to load metadata: {e}"))
}
}
fn check_for_locked_error(err: &str) -> bool {
err.contains("needs to be updated but --locked was passed to prevent this")
}
pub(crate) fn resolve_dep(
pkg: &Package,
dep: &Dependency,
meta: &Metadata,
) -> Option<RenamedPackage> {
match meta.resolve.as_ref() {
Some(resolve) => resolve_dep_from_graph(pkg, dep, (meta, resolve)),
None => resolve_dep_from_workspace(dep, meta),
}
}
pub(crate) fn resolve_dep_from_workspace(
dep: &Dependency,
meta: &Metadata,
) -> Option<RenamedPackage> {
for work in meta.workspace_packages() {
if work.name == dep.name {
let pkg = meta.packages.iter().find(|pkg| pkg.id == work.id).cloned();
return pkg.map(|pkg| RenamedPackage::new(pkg, dep.rename.clone(), dep.optional))
}
}
None
}
pub(crate) fn resolve_dep_from_graph(
pkg: &Package,
dep: &Dependency,
(meta, resolve): (&Metadata, &Resolve),
) -> Option<RenamedPackage> {
let dep_name = dep.rename.clone().unwrap_or(dep.name.clone()).replace('-', "_");
let resolved_pkg = resolve.nodes.iter().find(|node| node.id == pkg.id)?;
let resolved_dep_id = resolved_pkg.deps.iter().find(|node| node.name == dep_name)?;
let resolve_dep = meta.packages.iter().find(|pkg| pkg.id == resolved_dep_id.pkg)?;
Some(RenamedPackage::new(resolve_dep.clone(), dep.rename.clone(), dep.optional))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenamedPackage {
pub pkg: Package,
pub rename: Option<String>,
pub optional: bool,
}
impl RenamedPackage {
pub fn new(pkg: Package, rename: Option<String>, optional: bool) -> Self {
Self { pkg, rename, optional }
}
pub fn name(&self) -> String {
self.rename.clone().unwrap_or(self.pkg.name.to_string())
}
pub fn original_name(&self) -> String {
self.pkg.name.to_string()
}
pub fn display_name(&self) -> String {
match &self.rename {
Some(rename) => format!("{} (renamed from {})", rename, self.pkg.name),
None => self.pkg.name.to_string(),
}
}
}
impl Ord for RenamedPackage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(&self.pkg.name, &self.pkg.id).cmp(&(&other.pkg.name, &other.pkg.id))
}
}
impl PartialOrd for RenamedPackage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub(crate) fn parse_key_val<T, U>(
s: &str,
) -> Result<(T, U), Box<dyn std::error::Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: std::error::Error + Send + Sync + 'static,
{
let s = s.trim_matches('"');
let pos = s.find(':').ok_or_else(|| format!("invalid KEY=value: no `:` found in `{s}`"))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
pub(crate) fn check_can_modify<P: AsRef<Path>>(root: P, modify: P) -> Result<bool, String> {
let root = canonicalize(root).err_to_str()?;
let modify = canonicalize(modify).err_to_str()?;
if !modify.starts_with(&root) {
println!(
"Path is outside of the workspace: {:?} (not in {:?})",
modify.display(),
root.display()
);
Ok(false)
} else {
Ok(true)
}
}