#![forbid(unsafe_code)]
#![warn(rust_2018_idioms, unused_qualifications)]
use cargo_lock::{
Dependency, Lockfile, Package, ResolveVersion, Version,
dependency::Tree,
dependency::graph::EdgeDirection,
package::{self},
};
use clap::Parser;
use petgraph::graph::NodeIndex;
use std::{
env, fs, io,
path::{Path, PathBuf},
process::exit,
str::FromStr,
};
#[derive(Debug, Parser)]
#[command(name = "cargo-lock")]
enum Command {
List(ListCmd),
Translate(TranslateCmd),
Tree(TreeCmd),
}
#[derive(Debug, Parser)]
struct ListCmd {
#[arg(short, long)]
file: Option<PathBuf>,
#[arg(short, long)]
package: Option<package::Name>,
#[arg(short, long)]
dependencies: bool,
#[arg(short, long)]
sources: bool,
}
impl ListCmd {
fn run(&self) {
for package in &load_lockfile(&self.file).packages {
if let Some(name) = &self.package {
if &package.name != name {
continue;
}
}
if self.sources {
println!("- {}", Dependency::from(package));
} else {
println!("- {} {}", package.name, package.version);
}
if self.dependencies {
for dep in &package.dependencies {
if self.sources {
println!(" - {}", dep);
} else {
println!(" - {} {}", dep.name, dep.version);
}
}
}
}
}
}
#[derive(Debug, Parser)]
struct TranslateCmd {
#[arg(short, long)]
file: Option<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
version: Option<ResolveVersion>,
}
impl TranslateCmd {
fn run(&self) {
let output = self
.output
.as_ref()
.map(AsRef::as_ref)
.unwrap_or_else(|| Path::new("-"));
let mut lockfile = load_lockfile(&self.file);
lockfile.version = self.version.unwrap_or_default();
let lockfile_toml = lockfile.to_string();
if output == Path::new("-") {
println!("{}", lockfile_toml);
} else {
fs::write(output, lockfile_toml.as_bytes()).unwrap_or_else(|e| {
eprintln!("*** error: {}", e);
exit(1);
});
}
}
}
#[derive(Debug, Parser)]
struct TreeCmd {
#[arg(short, long)]
file: Option<PathBuf>,
#[arg(short = 'x', long)]
exact: bool,
#[arg(short, long = "invert")]
inverse: bool,
dependencies: Vec<String>,
}
fn package_matches_name(pkg: &Package, name: &str) -> bool {
pkg.name.as_str() == name
}
fn package_matches_ver(pkg: &Package, ver: &str) -> bool {
if let Ok(v) = Version::from_str(ver) {
return pkg.version == v;
}
if let Some(cksum) = &pkg.checksum {
if cksum.to_string() == ver {
return true;
}
}
if let Some(src) = &pkg.source {
if let Some(precise) = src.precise() {
if precise == ver {
return true;
}
}
}
false
}
fn package_matches(pkg: &Package, spec: &str) -> bool {
if let Some((name, ver)) = spec.split_once('@') {
package_matches_name(pkg, name) && package_matches_ver(pkg, ver)
} else {
package_matches_name(pkg, spec) || package_matches_ver(pkg, spec)
}
}
impl TreeCmd {
fn run(&self) {
let lockfile = load_lockfile(&self.file);
let tree = lockfile.dependency_tree().unwrap_or_else(|e| {
eprintln!("*** error: {}", e);
exit(1);
});
let indices: Vec<NodeIndex> = if self.dependencies.is_empty() {
tree.roots().to_vec()
} else {
self.dependencies
.iter()
.map(|dep| {
let package = lockfile
.packages
.iter()
.find(|pkg| package_matches(pkg, dep))
.unwrap_or_else(|| {
eprintln!("*** error: invalid dependency name: `{}`", dep);
exit(1);
});
tree.nodes()[&package.into()]
})
.collect()
};
self.dependency_tree(&tree, &indices);
}
fn dependency_tree(&self, tree: &Tree, indices: &[NodeIndex]) {
for (i, index) in indices.iter().enumerate() {
if i > 0 {
println!();
}
let direction = if self.inverse {
EdgeDirection::Incoming
} else {
EdgeDirection::Outgoing
};
tree.render(&mut io::stdout(), *index, direction, self.exact)
.unwrap();
}
}
}
fn load_lockfile(path: &Option<PathBuf>) -> Lockfile {
let path = path
.as_ref()
.map(AsRef::as_ref)
.unwrap_or_else(|| Path::new("Cargo.lock"));
Lockfile::load(path).unwrap_or_else(|e| {
eprintln!("*** error: {}", e);
exit(1);
})
}
fn main() {
let mut args = env::args().collect::<Vec<_>>();
if args.get(1).map(String::as_str) == Some("lock") {
args.remove(1);
}
if args.len() < 2 || args[1].starts_with('-') {
ListCmd::parse_from(&args).run();
return;
}
match Command::parse_from(&args) {
Command::List(list) => list.run(),
Command::Translate(translate) => translate.run(),
Command::Tree(tree) => tree.run(),
}
}