cargo-print 0.1.6

A cargo subcommand to print information in a shell-convenient format.
//! A cargo subcommand to print information in a shell-convenient format.
//!
//! <p style="font-family: 'Fira Sans',sans-serif;padding:0.3em 0"><strong>
//! <a href="https://crates.io/crates/cargo-print">📦&nbsp;&nbsp;Crates.io</a>&nbsp;&nbsp;│&nbsp;&nbsp;<a href="https://github.com/alecmocatta/cargo-print">📑&nbsp;&nbsp;GitHub</a>&nbsp;&nbsp;│&nbsp;&nbsp;<a href="https://constellation.zulipchat.com/#narrow/stream/213236-subprojects">💬&nbsp;&nbsp;Chat</a>
//! </strong></p>

#![warn(
	missing_copy_implementations,
	missing_debug_implementations,
	missing_docs,
	trivial_casts,
	trivial_numeric_casts,
	unused_import_braces,
	unused_qualifications,
	unused_results,
	clippy::pedantic
)] // from https://github.com/rust-unofficial/patterns/blob/master/anti_patterns/deny-warnings.md
#![allow(clippy::if_not_else, clippy::too_many_lines, clippy::let_underscore_drop)]

use cargo_metadata::{CargoOpt, MetadataCommand};
use std::{
	collections::{BTreeSet, HashMap, HashSet, VecDeque}, env, process
};
use walkdir::WalkDir;

fn main() {
	let mut args = env::args().skip(2);
	match args.next().as_deref() {
		Some("examples") => print_examples(args),
		Some("publish") => print_publish(args),
		Some("package") => print_package(args),
		Some("directory") => print_directory(args),
		Some("workspaces") => print_workspaces(args),
		Some("host") => print_host(args),
		_ => {
			eprintln!(
				"USAGE:\n    cargo print examples [--no-default-features] [--features <FEATURES>...] [--all-features]\n    cargo print publish\n    cargo print package\n    cargo print directory <package-name>\n    cargo print workspaces\n    cargo print host"
			);
			process::exit(1);
		}
	}
}

fn print_workspaces(mut args: impl Iterator<Item = String>) {
	if args.next().is_some() {
		eprintln!("USAGE:\n    cargo print workspaces");
		process::exit(1);
	}
	let cwd = env::current_dir().unwrap();
	let workspaces = WalkDir::new(".")
		.into_iter()
		.filter_map(|entry| {
			let entry = entry.expect("couldn't recurse fs");
			(entry.file_type().is_file() && entry.file_name() == "Cargo.toml")
				.then(|| MetadataCommand::new().manifest_path(entry.path()).exec().unwrap().workspace_root.strip_prefix(&cwd).unwrap().to_owned())
		})
		.collect::<BTreeSet<_>>();
	for workspace in workspaces {
		println!("{}", workspace);
	}
}

fn print_directory(mut args: impl Iterator<Item = String>) {
	let package_name = if let (Some(package_name), None) = (args.next(), args.next()) {
		package_name
	} else {
		eprintln!("USAGE:\n    cargo print directory");
		process::exit(1);
	};
	let metadata = MetadataCommand::new().exec().unwrap();
	let package = metadata.packages.into_iter().filter(|package| package.name == package_name).collect::<Vec<_>>();
	assert!(package.len() <= 1);
	assert!(package.len() == 1, "package {} not found", package_name);
	let package = package.into_iter().next().unwrap();
	let mut manifest_path = package.manifest_path;
	let _ = manifest_path.pop();
	println!("{}", manifest_path);
}

fn print_package(mut args: impl Iterator<Item = String>) {
	if args.next().is_some() {
		eprintln!("USAGE:\n    cargo print package");
		process::exit(1);
	}
	let current_dir = env::current_dir().unwrap();
	let current_manifest = current_dir.join("Cargo.toml");
	let metadata = MetadataCommand::new().exec().unwrap();
	let package = metadata.packages.into_iter().filter(|package| package.manifest_path == current_manifest).collect::<Vec<_>>();
	assert!(package.len() <= 1, "We seem to be in > 1 package {:?}", package);
	assert!(package.len() == 1, "We don't seem to be in a package");
	let package = package.into_iter().next().unwrap();
	println!("{}", package.name);
}

fn print_publish(mut args: impl Iterator<Item = String>) {
	if args.next().is_some() {
		eprintln!("USAGE:\n    cargo print publish");
		process::exit(1);
	}
	let metadata = MetadataCommand::new().exec().unwrap();
	let members = metadata.workspace_members.into_iter().collect::<HashSet<_>>();
	let members = metadata
		.packages
		.into_iter()
		.filter_map(|package| if members.contains(&package.id) { Some((package.name.clone(), package)) } else { None })
		.collect::<HashMap<_, _>>();
	let mut members: HashMap<String, HashSet<String>> = members
		.iter()
		.map(|(name, package)| {
			(
				name.clone(),
				package
					.dependencies
					.iter()
					.filter_map(|dep| if members.contains_key(&dep.name) { Some(dep.name.clone()) } else { None })
					.collect::<HashSet<String>>(),
			)
		})
		.collect::<HashMap<_, _>>();
	let mut dependents: HashMap<String, HashSet<String>> = HashMap::new();
	for (package, dependencies) in &members {
		for dependency in dependencies {
			let _ = dependents.entry(dependency.clone()).or_insert_with(HashSet::new).insert(package.clone());
		}
	}
	while !members.is_empty() {
		let publish = members.iter().find(|(_member, dependencies)| dependencies.is_empty()).expect("circular dependencies").0.clone();
		println!("{}", publish);
		let _ = members.remove(&publish).unwrap();
		for dependent in dependents.get(&publish).unwrap_or(&HashSet::new()) {
			let _ = members.get_mut(dependent).unwrap().remove(&publish);
		}
	}
}

fn print_examples(mut args: impl Iterator<Item = String>) {
	let mut opt_no_default_features = false;
	let mut opt_features = HashSet::new();
	let mut opt_all_features = false;
	let mut error = false;
	while let Some(arg) = args.next() {
		match &*arg {
			"--no-default-features" => opt_no_default_features = true,
			"--features" => {
				if let Some(features) = args.next() {
					for feature in features.split(' ').filter(|feature| !feature.is_empty()) {
						let _ = opt_features.insert(feature.to_owned());
					}
				} else {
					error = true;
				}
			}
			"--all-features" => opt_all_features = true,
			_ => error = true,
		}
	}
	if error {
		eprintln!("USAGE:\n    cargo print examples [--no-default-features] [--features <FEATURES>...] [--all-features]");
		process::exit(1);
	}
	let current_dir = env::current_dir().unwrap();
	let current_manifest = current_dir.join("Cargo.toml");
	let mut metadata = MetadataCommand::new();
	// TODO: what do these args actually do?
	if opt_no_default_features {
		let _ = metadata.features(CargoOpt::NoDefaultFeatures);
	}
	let _ = metadata.features(CargoOpt::SomeFeatures(opt_features.iter().cloned().collect()));
	if opt_all_features {
		let _ = metadata.features(CargoOpt::AllFeatures);
	}
	let metadata = metadata.exec().unwrap();
	let package = metadata.packages.into_iter().filter(|package| package.manifest_path == current_manifest).collect::<Vec<_>>();
	assert!(package.len() <= 1, "We seem to be in > 1 package {:?}", package);
	assert!(package.len() == 1, "We don't seem to be in a package");
	let package = package.into_iter().next().unwrap();
	let features = package
		.dependencies
		.into_iter()
		.filter_map(|dep| if dep.optional { Some((dep.rename.unwrap_or(dep.name), Vec::new())) } else { None })
		.chain(package.features.into_iter())
		.collect::<HashMap<String, Vec<String>>>();
	let features_set = features.keys().cloned().collect::<HashSet<_>>();
	let invalid_features = opt_features.difference(&features_set).collect::<Vec<_>>();
	if !invalid_features.is_empty() {
		println!("invalid feature {:?}", invalid_features);
		process::exit(1);
	}
	let mut enabled_features;
	if !opt_all_features {
		enabled_features = opt_features;
		if !opt_no_default_features && features.contains_key("default") {
			let _ = enabled_features.insert(String::from("default"));
		}
	} else {
		enabled_features = features.iter().map(|(k, _)| k.clone()).collect();
	}
	let mut features_stack = enabled_features.clone().into_iter().collect::<VecDeque<_>>();
	while let Some(feature) = features_stack.pop_front() {
		for feature in features.get(&feature).unwrap_or(&Vec::new()).iter() {
			if enabled_features.insert(feature.clone()) {
				features_stack.push_back(feature.clone());
			}
		}
	}
	let examples = package
		.targets
		.into_iter()
		.filter_map(|target| {
			if target.kind.contains(&String::from("example"))
				&& target.required_features.iter().cloned().collect::<HashSet<_>>().difference(&enabled_features).count() == 0
			{
				Some(target.name)
			} else {
				None
			}
		})
		.collect::<HashSet<_>>();
	for example in examples {
		println!("{}", example);
	}
}

fn print_host(mut args: impl Iterator<Item = String>) {
	if args.next().is_some() {
		eprintln!("USAGE:\n    cargo print host");
		process::exit(1);
	}
	println!("{}", env!("HOST"));
}