| use std::cell::RefCell; |
| use std::cmp::Ordering; |
| use std::collections::{BTreeSet, HashMap, VecDeque}; |
| use std::io::{self, Write}; |
| |
| use anyhow::anyhow; |
| use cargo::core::compiler::{CompileKind, RustcTargetData}; |
| use cargo::core::resolver::features::{ForceAllTargets, HasDevUnits}; |
| use cargo::core::{dependency::DepKind, Dependency, Package, PackageId, Workspace}; |
| use cargo::ops::{self, Packages}; |
| use cargo::util::{CargoResult, Config}; |
| use serde::{Deserialize, Serialize}; |
| use tabwriter::TabWriter; |
| |
| use super::pkg_status::*; |
| use super::Options; |
| |
| /// An elaborate workspace containing resolved dependencies and |
| /// the update status of packages |
| pub struct ElaborateWorkspace<'ela> { |
| pub workspace: &'ela Workspace<'ela>, |
| pub pkgs: HashMap<PackageId, Package>, |
| pub pkg_deps: HashMap<PackageId, HashMap<PackageId, Dependency>>, |
| /// Map of package status |
| pub pkg_status: RefCell<HashMap<Vec<PackageId>, PkgStatus>>, |
| /// Whether using workspace mode |
| pub workspace_mode: bool, |
| } |
| |
| /// A struct to serialize to json with serde |
| #[derive(Serialize, Deserialize)] |
| pub struct CrateMetadata { |
| pub crate_name: String, |
| pub dependencies: BTreeSet<Metadata>, |
| } |
| |
| #[derive(Serialize, Deserialize, Eq, PartialEq)] |
| pub struct Metadata { |
| pub name: String, |
| pub project: String, |
| pub compat: String, |
| pub latest: String, |
| pub kind: Option<String>, |
| pub platform: Option<String>, |
| } |
| |
| impl Ord for Metadata { |
| fn cmp(&self, other: &Self) -> Ordering { self.name.cmp(&other.name) } |
| } |
| |
| impl PartialOrd for Metadata { |
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } |
| } |
| |
| impl<'ela> ElaborateWorkspace<'ela> { |
| /// Elaborate a `Workspace` |
| pub fn from_workspace( |
| workspace: &'ela Workspace<'_>, |
| options: &Options, |
| ) -> CargoResult<ElaborateWorkspace<'ela>> { |
| use cargo::core::resolver::{features::RequestedFeatures, ResolveOpts}; |
| let specs = Packages::All.to_package_id_specs(workspace)?; |
| let features = RequestedFeatures::from_command_line( |
| &options.flag_features, |
| options.all_features(), |
| options.no_default_features(), |
| ); |
| let opts = ResolveOpts::new(true, features); |
| //The CompileKind, this has no target since it's the temp workspace |
| //targets are blank since we don't need to fully build for the targets to get the dependencies |
| let compile_kind = CompileKind::from_requested_targets(workspace.config(), &[])?; |
| let target_data = RustcTargetData::new(&workspace, &compile_kind)?; |
| let ws_resolve = ops::resolve_ws_with_opts( |
| &workspace, |
| &target_data, |
| &compile_kind, |
| &opts, |
| &specs, |
| HasDevUnits::Yes, |
| ForceAllTargets::Yes, |
| )?; |
| let packages = ws_resolve.pkg_set; |
| let resolve = ws_resolve |
| .workspace_resolve |
| .expect("Error getting workspace resolved"); |
| let mut pkgs = HashMap::new(); |
| let mut pkg_deps = HashMap::new(); |
| for pkg in packages.get_many(packages.package_ids())? { |
| let pkg_id = pkg.package_id(); |
| pkgs.insert(pkg_id, pkg.clone()); |
| let deps = pkg.dependencies(); |
| let mut dep_map = HashMap::new(); |
| for dep_id in resolve.deps(pkg_id) { |
| for d in deps { |
| if d.matches_id(dep_id.0) { |
| dep_map.insert(dep_id.0, d.clone()); |
| break; |
| } |
| } |
| } |
| pkg_deps.insert(pkg_id, dep_map); |
| } |
| |
| Ok(ElaborateWorkspace { |
| workspace, |
| pkgs, |
| pkg_deps, |
| pkg_status: RefCell::new(HashMap::new()), |
| workspace_mode: options.flag_workspace || workspace.current().is_err(), |
| }) |
| } |
| |
| /// Determine root package based on current workspace and CLI options |
| pub fn determine_root(&self, options: &Options) -> CargoResult<PackageId> { |
| if let Some(ref root_name) = options.flag_root { |
| if let Ok(workspace_root) = self.workspace.current() { |
| if root_name == workspace_root.name().as_str() { |
| Ok(workspace_root.package_id()) |
| } else { |
| for direct_dep in self.pkg_deps[&workspace_root.package_id()].keys() { |
| if self.pkgs[direct_dep].name().as_str() == root_name { |
| return Ok(*direct_dep); |
| } |
| } |
| Err(anyhow!( |
| "Root is neither the workspace root nor a direct dependency", |
| )) |
| } |
| } else { |
| Err(anyhow!( |
| "--root is not allowed when running against a virtual manifest", |
| )) |
| } |
| } else { |
| Ok(self.workspace.current()?.package_id()) |
| } |
| } |
| |
| /// Find a member based on member name |
| fn find_member(&self, member: PackageId) -> CargoResult<PackageId> { |
| for m in self.workspace.members() { |
| // members with the same name in a workspace is not allowed |
| // even with different paths |
| if member.name() == m.name() { |
| return Ok(m.package_id()); |
| } |
| } |
| Err(anyhow!("Workspace member {} not found", member.name())) |
| } |
| |
| /// Find a contained package, which is a member or dependency inside the workspace |
| fn find_contained_package(&self, name: &str) -> CargoResult<PackageId> { |
| let root_path = self.workspace.root(); |
| for (pkg_id, pkg) in &self.pkgs { |
| if pkg.manifest_path().starts_with(root_path) && pkg.name().as_str() == name { |
| return Ok(*pkg_id); |
| } |
| } |
| Err(anyhow!("Cannot find package {} in workspace", name)) |
| } |
| |
| /// Find a direct dependency of a contained package |
| pub fn find_direct_dependency( |
| &self, |
| dependency_name: &str, |
| dependent_package_name: &str, |
| ) -> CargoResult<PackageId> { |
| let dependent_package = self.find_contained_package(dependent_package_name)?; |
| |
| for direct_dep in self.pkg_deps[&dependent_package].keys() { |
| if direct_dep.name().as_str() == dependency_name { |
| return Ok(*direct_dep); |
| } |
| } |
| |
| for (pkg_id, pkg) in &self.pkgs { |
| if pkg.name().as_str() == dependency_name { |
| return Ok(*pkg_id); |
| } |
| } |
| |
| Err(anyhow!( |
| "Direct dependency {} not found for package {}", |
| dependency_name, |
| dependent_package_name |
| )) |
| } |
| |
| /// Resolve compatible and latest status from the corresponding `ElaborateWorkspace`s |
| pub fn resolve_status( |
| &'ela self, |
| compat: &ElaborateWorkspace<'_>, |
| latest: &ElaborateWorkspace<'_>, |
| options: &Options, |
| _config: &Config, |
| root: PackageId, |
| ) -> CargoResult<()> { |
| self.pkg_status.borrow_mut().clear(); |
| let (compat_root, latest_root) = if self.workspace_mode { |
| (compat.find_member(root)?, latest.find_member(root)?) |
| } else { |
| ( |
| compat.determine_root(options)?, |
| latest.determine_root(options)?, |
| ) |
| }; |
| |
| let mut queue = VecDeque::new(); |
| queue.push_back((vec![root], Some(compat_root), Some(latest_root))); |
| while let Some((path, compat_pkg, latest_pkg)) = queue.pop_front() { |
| let pkg = path.last().unwrap(); |
| let depth = path.len() as i32 - 1; |
| // generate pkg_status |
| let status = PkgStatus { |
| compat: Status::from_versions(pkg.version(), compat_pkg.map(PackageId::version)), |
| latest: Status::from_versions(pkg.version(), latest_pkg.map(PackageId::version)), |
| }; |
| debug!( |
| _config, |
| "STATUS => PKG: {}; PATH: {:?}; COMPAT: {:?}; LATEST: {:?}; STATUS: {:?}", |
| pkg, |
| path, |
| compat_pkg, |
| latest_pkg, |
| status |
| ); |
| self.pkg_status.borrow_mut().insert(path.clone(), status); |
| // next layer |
| if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() { |
| self.pkg_deps[pkg] |
| .keys() |
| .filter(|dep| !path.contains(dep)) |
| .for_each(|&dep| { |
| let name = dep.name(); |
| let compat_pkg = compat_pkg |
| .and_then(|id| compat.pkg_deps.get(&id)) |
| .map(HashMap::keys) |
| .and_then(|mut deps| deps.find(|dep| dep.name() == name)) |
| .cloned(); |
| let latest_pkg = latest_pkg |
| .and_then(|id| latest.pkg_deps.get(&id)) |
| .map(HashMap::keys) |
| .and_then(|mut deps| deps.find(|dep| dep.name() == name)) |
| .cloned(); |
| let mut path = path.clone(); |
| path.push(dep); |
| queue.push_back((path, compat_pkg, latest_pkg)); |
| }); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Print package status to `TabWriter` |
| pub fn print_list( |
| &'ela self, |
| options: &Options, |
| root: PackageId, |
| preceding_line: bool, |
| ) -> CargoResult<i32> { |
| let mut lines = BTreeSet::new(); |
| let mut queue = VecDeque::new(); |
| queue.push_back(vec![root]); |
| while let Some(path) = queue.pop_front() { |
| let pkg = path.last().unwrap(); |
| let name = pkg.name().to_string(); |
| |
| if options.flag_ignore.contains(&name) { |
| continue; |
| } |
| |
| let depth = path.len() as i32 - 1; |
| // generate lines |
| let status = &self.pkg_status.borrow_mut()[&path]; |
| if (status.compat.is_changed() || status.latest.is_changed()) |
| && (options.flag_packages.is_empty() || options.flag_packages.contains(&name)) |
| { |
| // name version compatible latest kind platform |
| let parent = path.get(path.len() - 2); |
| if let Some(parent) = parent { |
| let dependency = &self.pkg_deps[parent][pkg]; |
| let label = if self.workspace_mode |
| || parent == &self.workspace.current()?.package_id() |
| { |
| name |
| } else { |
| format!("{}->{}", self.pkgs[parent].name(), name) |
| }; |
| let line = format!( |
| "{}\t{}\t{}\t{}\t{:?}\t{}\n", |
| label, |
| pkg.version(), |
| status.compat.to_string(), |
| status.latest.to_string(), |
| dependency.kind(), |
| dependency |
| .platform() |
| .map(ToString::to_string) |
| .unwrap_or_else(|| "---".to_owned()) |
| ); |
| lines.insert(line); |
| } else { |
| let line = format!( |
| "{}\t{}\t{}\t{}\t---\t---\n", |
| name, |
| pkg.version(), |
| status.compat.to_string(), |
| status.latest.to_string() |
| ); |
| lines.insert(line); |
| } |
| } |
| // next layer |
| if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() { |
| self.pkg_deps[pkg] |
| .keys() |
| .filter(|dep| !path.contains(dep)) |
| .filter(|&dep| { |
| !self.workspace_mode |
| || !self.workspace.members().any(|mem| &mem.package_id() == dep) |
| }) |
| .for_each(|&dep| { |
| let mut path = path.clone(); |
| path.push(dep); |
| queue.push_back(path); |
| }); |
| } |
| } |
| |
| if lines.is_empty() { |
| if !self.workspace_mode { |
| println!("All dependencies are up to date, yay!"); |
| } |
| } else { |
| if preceding_line { |
| println!(); |
| } |
| if self.workspace_mode { |
| println!("{}\n================", root.name()); |
| } |
| let mut tw = TabWriter::new(vec![]); |
| writeln!(&mut tw, "Name\tProject\tCompat\tLatest\tKind\tPlatform")?; |
| writeln!(&mut tw, "----\t-------\t------\t------\t----\t--------")?; |
| for line in &lines { |
| write!(&mut tw, "{}", line)?; |
| } |
| tw.flush()?; |
| write!( |
| io::stdout(), |
| "{}", |
| String::from_utf8(tw.into_inner().unwrap()).unwrap() |
| )?; |
| io::stdout().flush()?; |
| } |
| |
| Ok(lines.len() as i32) |
| } |
| |
| pub fn print_json(&'ela self, options: &Options, root: PackageId) -> CargoResult<i32> { |
| let mut crate_graph = CrateMetadata { |
| crate_name: root.name().to_string(), |
| dependencies: BTreeSet::new(), |
| }; |
| let mut queue = VecDeque::new(); |
| queue.push_back(vec![root]); |
| |
| while let Some(path) = queue.pop_front() { |
| let pkg = path.last().unwrap(); |
| let name = pkg.name().to_string(); |
| |
| if options.flag_ignore.contains(&name) { |
| continue; |
| } |
| |
| let depth = path.len() as i32 - 1; |
| // generate lines |
| let status = &self.pkg_status.borrow_mut()[&path]; |
| if (status.compat.is_changed() || status.latest.is_changed()) |
| && (options.flag_packages.is_empty() || options.flag_packages.contains(&name)) |
| { |
| // name version compatible latest kind platform |
| let parent = path.get(path.len() - 2); |
| |
| let line; |
| |
| if let Some(parent) = parent { |
| let dependency = &self.pkg_deps[parent][pkg]; |
| let label = if self.workspace_mode |
| || parent == &self.workspace.current()?.package_id() |
| { |
| name |
| } else { |
| format!("{}->{}", self.pkgs[parent].name(), name) |
| }; |
| |
| let dependency_type = match dependency.kind() { |
| DepKind::Normal => "Normal", |
| DepKind::Development => "Development", |
| DepKind::Build => "Build", |
| }; |
| |
| line = Metadata { |
| name: label, |
| project: pkg.version().to_string(), |
| compat: status.compat.to_string(), |
| latest: status.latest.to_string(), |
| kind: Some(dependency_type.to_string()), |
| platform: dependency.platform().map(|p| p.to_string()), |
| }; |
| } else { |
| line = Metadata { |
| name, |
| project: pkg.version().to_string(), |
| compat: status.compat.to_string(), |
| latest: status.latest.to_string(), |
| kind: None, |
| platform: None, |
| }; |
| } |
| |
| crate_graph.dependencies.insert(line); |
| } |
| // next layer |
| if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() { |
| self.pkg_deps[pkg] |
| .keys() |
| .filter(|dep| !path.contains(dep)) |
| .filter(|dep| { |
| !self.workspace_mode |
| || !self |
| .workspace |
| .members() |
| .any(|mem| mem.package_id() == **dep) |
| }) |
| .for_each(|dep| { |
| let mut path = path.clone(); |
| path.push(*dep); |
| queue.push_back(path); |
| }); |
| } |
| } |
| |
| println!("{}", serde_json::to_string(&crate_graph)?); |
| |
| Ok(crate_graph.dependencies.len() as i32) |
| } |
| } |