//! Shared logic for `depends` and `prohibits` commands. //! //! `depends` (aka `why`) answers: "Which packages require package X?" //! `prohibits` (aka `why-not`) answers: "Which packages prevent version X of package Y from being //! installed?" use indexmap::IndexSet; use std::collections::BTreeMap; use std::path::Path; use anyhow::Result; use mozart_core::console_format; use mozart_core::console_writeln; /// Inputs for [`do_execute`], collected from the `depends` / `prohibits` CLI args. pub struct DoExecuteArgs<'a> { pub package: &'a str, /// Version constraint string (only set for `prohibits`). pub version: Option<&'a str>, pub recursive: bool, pub tree: bool, pub locked: bool, /// `true` for `prohibits` (why-not), `false` for `depends` (why). pub inverted: bool, } /// Shared implementation for `depends` (why) and `prohibits` (why-not). /// /// Mirrors `BaseDependencyCommand::doExecute` in Composer: a single function /// driven by `inverted` to switch between "who depends on X?" and /// "who prevents X version V from being installed?". pub fn do_execute( cli: &super::Cli, console: &mozart_core::console::Console, args: DoExecuteArgs<'_>, ) -> Result<()> { let DoExecuteArgs { package, version, recursive, tree, locked, inverted, } = args; let working_dir = cli.working_dir()?; let packages = load_packages(&working_dir, locked)?; if packages.is_empty() { console.write_error( "No dependencies installed. Try running mozart install or update, or use --locked.", ); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); } let target = package.to_lowercase(); let target_known = packages.iter().any(|p| p.name.to_lowercase() == target); if !target_known { if !inverted && mozart_core::platform::is_platform_package(&target) { anyhow::bail!( "Could not find platform package \"{}\". Is PHP available?", package ); } anyhow::bail!("Could not find package \"{}\" in your project", package); } let constraint = match version { Some(v) => Some( mozart_semver::VersionConstraint::parse(v) .map_err(|e| anyhow::anyhow!("Invalid version constraint '{}': {}", v, e))?, ), None => None, }; let recursive = tree || recursive; let needles = vec![target]; let results = get_dependents( &packages, &needles, constraint.as_ref(), inverted, recursive, )?; if results.is_empty() { if inverted { console_writeln!( console, &console_format!( "{} {} can be installed.", package, version.unwrap_or("") ), ); return Ok(()); } console.info(&format!( "There is no installed package depending on \"{}\"", package )); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); } if tree { print_tree(&results, 0, console); } else { print_table(&results, console); } if !inverted { return Ok(()); } // Resolution hint: pick the right composer command based on whether the // package sits in root's `require`, `require-dev`, or neither. let needle_lower = package.to_lowercase(); let composer_command = packages .iter() .find(|p| p.is_root) .map(|root| { if root .require .keys() .any(|k| k.to_lowercase() == needle_lower) { "require" } else if root .require_dev .keys() .any(|k| k.to_lowercase() == needle_lower) { "require --dev" } else { "update" } }) .unwrap_or("update"); console.info(&format!( "Not finding what you were looking for? Try calling `composer {} \"{}:{}\" --dry-run` to get another view on the problem.", composer_command, package, version.unwrap_or("") )); Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )) } /// Normalised view of a package's dependency information. #[derive(Debug, Clone)] pub struct PackageInfo { pub name: String, pub version: String, /// Runtime requirements (`require` section). pub require: BTreeMap, /// Dev requirements (`require-dev`) — only non-empty for the root package. pub require_dev: BTreeMap, /// Conflict declarations (`conflict` section). pub conflict: BTreeMap, /// Whether this is the root `composer.json` package. pub is_root: bool, } /// A single result node in the dependency graph walk. #[derive(Debug, Clone)] pub struct DependencyResult { /// Name of the package that has the link. pub package_name: String, /// Version of the package that has the link. pub package_version: String, /// Human-readable link type: "requires", "requires (dev)", or "conflicts". pub link_description: String, /// The target package name (the one being queried). pub link_target: String, /// The constraint string from the link (e.g. "^1.0"). pub link_constraint: String, /// Children found during a recursive walk (empty for flat results). pub children: Vec, } /// Load all packages relevant to the dependency query. /// /// When `locked` is true (or the lock file exists), reads from `composer.lock`. /// Otherwise falls back to `vendor/composer/installed.json`. /// The root `composer.json` is always added as a synthetic entry. pub fn load_packages(working_dir: &Path, locked: bool) -> Result> { let lock_path = working_dir.join("composer.lock"); let composer_json_path = working_dir.join("composer.json"); // Load locked / installed packages let mut packages: Vec = if locked { load_from_lockfile(&lock_path)? } else { let installed = load_from_installed(working_dir); match installed { Ok(pkgs) if !pkgs.is_empty() => pkgs, _ => { if lock_path.exists() { load_from_lockfile(&lock_path)? } else { vec![] } } } }; // Add platform packages (php, ext-*, lib-*, composer-*-api) let platform = mozart_core::platform::detect_platform(); for pp in &platform { packages.push(PackageInfo { name: pp.name.clone(), version: pp.version.clone(), require: BTreeMap::new(), require_dev: BTreeMap::new(), conflict: BTreeMap::new(), is_root: false, }); } // Add the root package (composer.json) as a synthetic entry if composer_json_path.exists() && let Ok(root) = mozart_core::package::read_from_file(&composer_json_path) { // Extract conflict from extra_fields if present let conflict: BTreeMap = root .extra_fields .get("conflict") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_default(); packages.push(PackageInfo { name: root.name.clone(), version: "ROOT".to_string(), require: root.require, require_dev: root.require_dev, conflict, is_root: true, }); } Ok(packages) } fn load_from_lockfile(lock_path: &Path) -> Result> { if !lock_path.exists() { anyhow::bail!("composer.lock not found — run `mozart install` first or omit --locked"); } let lock = mozart_registry::lockfile::LockFile::read_from_file(lock_path)?; let mut packages: Vec = Vec::new(); for pkg in &lock.packages { packages.push(PackageInfo { name: pkg.name.clone(), version: pkg.version.clone(), require: pkg.require.clone(), require_dev: BTreeMap::new(), // locked packages don't expose require-dev conflict: pkg.conflict.clone(), is_root: false, }); } if let Some(ref dev_pkgs) = lock.packages_dev { for pkg in dev_pkgs { packages.push(PackageInfo { name: pkg.name.clone(), version: pkg.version.clone(), require: pkg.require.clone(), require_dev: BTreeMap::new(), conflict: pkg.conflict.clone(), is_root: false, }); } } Ok(packages) } fn load_from_installed(working_dir: &Path) -> Result> { let vendor_dir = working_dir.join("vendor"); let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?; let packages = installed .packages .iter() .map(|p| { // InstalledPackageEntry uses extra_fields for require/conflict; we do a best-effort // extraction since installed.json doesn't always carry full dep info. let require = p .extra_fields .get("require") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_default(); let conflict = p .extra_fields .get("conflict") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_default(); PackageInfo { name: p.name.clone(), version: p.version.clone(), require, require_dev: BTreeMap::new(), conflict, is_root: false, } }) .collect(); Ok(packages) } /// Find all packages that have a dependency relationship with the needle(s). /// /// * `packages` — the full set of packages to search through. /// * `needles` — package names to look for (usually just one). /// * `constraint` — when `Some`, used for the `prohibits` check (see below). /// * `inverted` — if `true`, run the "prohibits" logic instead of "depends": /// - A package *prohibits* version V of package P if: /// a) it **requires** P with a constraint that does NOT match V, OR /// b) it **conflicts** with P at a constraint that matches V. /// * `recursive` — walk transitively up to the root. pub fn get_dependents( packages: &[PackageInfo], needles: &[String], constraint: Option<&mozart_semver::VersionConstraint>, inverted: bool, recursive: bool, ) -> Result> { if inverted { get_prohibitors(packages, needles, constraint, recursive) } else { get_dependents_forward(packages, needles, recursive) } } fn get_dependents_forward( packages: &[PackageInfo], needles: &[String], recursive: bool, ) -> Result> { let needle_set: IndexSet = needles.iter().map(|n| n.to_lowercase()).collect(); // Build name→PackageInfo lookup let pkg_map: BTreeMap = packages .iter() .map(|p| (p.name.to_lowercase(), p)) .collect(); if recursive { // Recursive: BFS from needles upward to root, building a tree let mut visited: IndexSet = IndexSet::new(); let mut results: Vec = Vec::new(); for needle in needles { let needle_lower = needle.to_lowercase(); let direct = collect_direct_requires(packages, &needle_lower); for mut result in direct { let pkg_lower = result.package_name.to_lowercase(); if visited.insert(pkg_lower.clone()) { // Recurse: who requires this package? result.children = recurse_dependents( packages, &pkg_lower, &pkg_map, &mut visited, &needle_set, ); results.push(result); } } } Ok(results) } else { // Flat: just direct dependents let mut results: Vec = Vec::new(); for needle in needles { let needle_lower = needle.to_lowercase(); results.extend(collect_direct_requires(packages, &needle_lower)); } Ok(results) } } /// Collect all packages that directly require `needle`. fn collect_direct_requires(packages: &[PackageInfo], needle: &str) -> Vec { let mut results = Vec::new(); for pkg in packages { // Check `require` if let Some((target, constraint)) = pkg.require.iter().find(|(k, _)| k.to_lowercase() == needle) { results.push(DependencyResult { package_name: pkg.name.clone(), package_version: pkg.version.clone(), link_description: "requires".to_string(), link_target: target.clone(), link_constraint: constraint.clone(), children: vec![], }); } // Check `require-dev` (root package only) if pkg.is_root && let Some((target, constraint)) = pkg .require_dev .iter() .find(|(k, _)| k.to_lowercase() == needle) { results.push(DependencyResult { package_name: pkg.name.clone(), package_version: pkg.version.clone(), link_description: "requires (dev)".to_string(), link_target: target.clone(), link_constraint: constraint.clone(), children: vec![], }); } } results } /// Recursively find who requires `needle` (used by recursive depends). fn recurse_dependents( packages: &[PackageInfo], needle: &str, pkg_map: &BTreeMap, visited: &mut IndexSet, _original_needles: &IndexSet, ) -> Vec { let _ = pkg_map; // kept for potential future use let direct = collect_direct_requires(packages, needle); let mut results = Vec::new(); for mut result in direct { let pkg_lower = result.package_name.to_lowercase(); if visited.insert(pkg_lower.clone()) { result.children = recurse_dependents(packages, &pkg_lower, pkg_map, visited, _original_needles); results.push(result); } } results } fn get_prohibitors( packages: &[PackageInfo], needles: &[String], constraint: Option<&mozart_semver::VersionConstraint>, _recursive: bool, ) -> Result> { let mut results: Vec = Vec::new(); for needle in needles { let needle_lower = needle.to_lowercase(); for pkg in packages { // Case 1: package requires the needle, but the required constraint // does NOT match the requested version (i.e. it would reject it). if let Some((target, req_constraint_str)) = pkg .require .iter() .find(|(k, _)| k.to_lowercase() == needle_lower) && let Some(requested_version) = constraint && let Ok(pkg_constraint) = mozart_semver::VersionConstraint::parse(req_constraint_str) { // The package requires `needle` but with a different // (incompatible) constraint — it blocks the requested version. // We check: does any version satisfying the requested constraint // NOT satisfy the package's constraint? if constraint_prohibits(requested_version, &pkg_constraint) { results.push(DependencyResult { package_name: pkg.name.clone(), package_version: pkg.version.clone(), link_description: "requires".to_string(), link_target: target.clone(), link_constraint: req_constraint_str.clone(), children: vec![], }); } } // Also check require-dev for root if pkg.is_root && let Some((target, req_constraint_str)) = pkg .require_dev .iter() .find(|(k, _)| k.to_lowercase() == needle_lower) && let Some(requested_version) = constraint && let Ok(pkg_constraint) = mozart_semver::VersionConstraint::parse(req_constraint_str) && constraint_prohibits(requested_version, &pkg_constraint) { results.push(DependencyResult { package_name: pkg.name.clone(), package_version: pkg.version.clone(), link_description: "requires (dev)".to_string(), link_target: target.clone(), link_constraint: req_constraint_str.clone(), children: vec![], }); } // Case 2: package *conflicts* with the needle at a version that // overlaps with the requested version (i.e. the conflict blocks it). if let Some((target, conflict_constraint_str)) = pkg .conflict .iter() .find(|(k, _)| k.to_lowercase() == needle_lower) && let Some(requested_version) = constraint && let Ok(conflict_constraint) = mozart_semver::VersionConstraint::parse(conflict_constraint_str) { // If the conflict constraint overlaps with (matches) the // requested version range, this package conflicts with it. if constraint_overlaps(requested_version, &conflict_constraint) { results.push(DependencyResult { package_name: pkg.name.clone(), package_version: pkg.version.clone(), link_description: "conflicts".to_string(), link_target: target.clone(), link_constraint: conflict_constraint_str.clone(), children: vec![], }); } } } } Ok(results) } /// Returns `true` if `requested` (the version the user wants to install) is /// **not** matched by `pkg_constraint` (the constraint the installed package /// requires), meaning the installed package would block installation. /// /// We sample a set of "representative versions" from the requested constraint /// and check whether none of them satisfy the package's constraint. fn constraint_prohibits( requested: &mozart_semver::VersionConstraint, pkg_constraint: &mozart_semver::VersionConstraint, ) -> bool { // We try to determine if there is any version satisfying *requested* that // does NOT satisfy *pkg_constraint*. // Strategy: collect "probe" versions that the requested constraint implies, // then check if any probe is rejected by pkg_constraint. let probes = sample_versions_from_constraint(requested); if probes.is_empty() { // Cannot determine — report as prohibiting return true; } // If ANY probe satisfies the requested constraint but NOT pkg_constraint → prohibits probes .iter() .any(|v| requested.matches(v) && !pkg_constraint.matches(v)) } /// Returns `true` if the conflict constraint overlaps with the requested version. /// That is, if the conflict constraint matches at least one version that the /// requested constraint also matches. fn constraint_overlaps( requested: &mozart_semver::VersionConstraint, conflict_constraint: &mozart_semver::VersionConstraint, ) -> bool { let probes = sample_versions_from_constraint(requested); if probes.is_empty() { return true; } probes .iter() .any(|v| requested.matches(v) && conflict_constraint.matches(v)) } /// Generate a small set of concrete `Version` values that probe the shape of a /// constraint. These are used for the "does this constraint overlap/prohibit /// that constraint?" heuristic. fn sample_versions_from_constraint( constraint: &mozart_semver::VersionConstraint, ) -> Vec { use mozart_semver::Version; // Broad grid of versions to probe let candidates: &[&str] = &[ "0.0.1", "0.1.0", "0.9.0", "1.0.0", "1.0.1", "1.1.0", "1.2.0", "1.5.0", "1.9.0", "1.9.9", "2.0.0", "2.1.0", "2.5.0", "2.9.9", "3.0.0", "3.1.0", "3.9.9", "4.0.0", "4.9.0", "5.0.0", "6.0.0", "7.0.0", "8.0.0", "9.0.0", "10.0.0", "0.0.1-alpha1", "1.0.0-beta1", "1.0.0-RC1", ]; candidates .iter() .filter_map(|s| Version::parse(s).ok()) .filter(|v| constraint.matches(v)) .collect() } /// Print results as a flat table. /// /// Columns: package name | version | link description | link constraint pub fn print_table(results: &[DependencyResult], console: &mozart_core::console::Console) { if results.is_empty() { console_writeln!( console, &console_format!("No relationships found."), ); return; } // Column widths let name_w = results .iter() .map(|r| r.package_name.len()) .max() .unwrap_or(0); let ver_w = results .iter() .map(|r| r.package_version.len()) .max() .unwrap_or(0); let desc_w = results .iter() .map(|r| r.link_description.len()) .max() .unwrap_or(0); let mut seen: IndexSet = IndexSet::new(); for r in results { let key = format!( "{}|{}|{}|{}", r.package_name, r.package_version, r.link_description, r.link_constraint ); if !seen.insert(key) { continue; } console_writeln!( console, &format!( "{:{}", r.package_name), console_format!("{}", r.package_version), r.link_description, console_format!("{}", r.link_constraint), name_w = name_w, ver_w = ver_w, desc_w = desc_w, ), ); } } /// Print results as a nested tree using box-drawing characters. /// /// Example output: /// /// ```text /// vendor/a 1.0.0 requires ^1.0 /// └─ vendor/b 2.0.0 requires ^2.0 /// └─ root/project ROOT requires ^2.0 /// ``` pub fn print_tree( results: &[DependencyResult], depth: usize, console: &mozart_core::console::Console, ) { if results.is_empty() && depth == 0 { console_writeln!( console, &console_format!("No relationships found."), ); return; } let count = results.len(); for (i, r) in results.iter().enumerate() { let is_last = i + 1 == count; let prefix = tree_prefix(depth, is_last); console_writeln!( console, &format!( "{}{:<} {} {} {}", prefix, console_format!("{}", r.package_name), console_format!("{}", r.package_version), r.link_description, console_format!("{}", r.link_constraint), ), ); if !r.children.is_empty() { print_tree(&r.children, depth + 1, console); } } } fn tree_prefix(depth: usize, is_last: bool) -> String { if depth == 0 { return String::new(); } let indent = " ".repeat(depth - 1); let branch = if is_last { "└─ " } else { "├─ " }; format!("{indent}{branch}") } #[cfg(test)] mod tests { use super::*; fn make_pkg( name: &str, version: &str, require: &[(&str, &str)], conflict: &[(&str, &str)], is_root: bool, ) -> PackageInfo { PackageInfo { name: name.to_string(), version: version.to_string(), require: require .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), require_dev: BTreeMap::new(), conflict: conflict .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), is_root, } } #[test] fn test_forward_dependency() { // root requires A, A requires B → depends B returns A (and root not A) let packages = vec![ make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^2.0")], &[], false), make_pkg("vendor/b", "2.0.0", &[], &[], false), ]; let needles = vec!["vendor/b".to_string()]; let results = get_dependents(&packages, &needles, None, false, false).unwrap(); assert_eq!(results.len(), 1, "Only A requires B directly"); assert_eq!(results[0].package_name, "vendor/a"); assert_eq!(results[0].link_description, "requires"); assert_eq!(results[0].link_constraint, "^2.0"); } #[test] fn test_recursive_dependency() { // root requires A, A requires B → depends B --recursive returns A, with root as child let packages = vec![ make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^2.0")], &[], false), make_pkg("vendor/b", "2.0.0", &[], &[], false), ]; let needles = vec!["vendor/b".to_string()]; let results = get_dependents(&packages, &needles, None, false, true).unwrap(); // A is found as direct dependent of B assert!(!results.is_empty()); let a_result = results.iter().find(|r| r.package_name == "vendor/a"); assert!(a_result.is_some(), "vendor/a should be found"); // root should appear as a child of vendor/a let children = &a_result.unwrap().children; assert!( children.iter().any(|c| c.package_name == "root/project"), "root/project should be a child of vendor/a" ); } #[test] fn test_no_dependents() { // Nothing requires X let packages = vec![ make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), make_pkg("vendor/a", "1.0.0", &[], &[], false), ]; let needles = vec!["vendor/x".to_string()]; let results = get_dependents(&packages, &needles, None, false, false).unwrap(); assert!(results.is_empty()); } #[test] fn test_circular_detection() { // A requires B, B requires A — should not loop forever let packages = vec![ make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^1.0")], &[], false), make_pkg("vendor/b", "1.0.0", &[("vendor/a", "^1.0")], &[], false), ]; let needles = vec!["vendor/b".to_string()]; // Should terminate without stack overflow let results = get_dependents(&packages, &needles, None, false, true).unwrap(); // vendor/a requires vendor/b → found; vendor/b would recurse back to vendor/a // but visited set prevents infinite loop assert!(!results.is_empty()); } #[test] fn test_prohibits_basic() { // root requires A ^1.0; user asks "who prohibits A 2.0" // → root requires A ^1.0 which doesn't match 2.0 → root prohibits it let packages = vec![ make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), make_pkg("vendor/a", "1.0.0", &[], &[], false), ]; let constraint = mozart_semver::VersionConstraint::parse("2.0.0").unwrap(); let needles = vec!["vendor/a".to_string()]; let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); assert!(!results.is_empty(), "root should prohibit vendor/a 2.0"); assert_eq!(results[0].package_name, "root/project"); assert_eq!(results[0].link_description, "requires"); } #[test] fn test_prohibits_conflict_field() { // pkg/b conflicts with vendor/a ^2.0 → prohibits vendor/a 2.0 let packages = vec![ make_pkg( "root/project", "ROOT", &[("vendor/a", "^1.0"), ("vendor/b", "^1.0")], &[], true, ), make_pkg("vendor/a", "1.0.0", &[], &[], false), make_pkg( "vendor/b", "1.0.0", &[], &[("vendor/a", "^2.0")], // conflicts with vendor/a ^2.0 false, ), ]; let constraint = mozart_semver::VersionConstraint::parse("2.0.0").unwrap(); let needles = vec!["vendor/a".to_string()]; let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); // vendor/b conflicts with vendor/a ^2.0 which covers 2.0.0 let conflict_result = results.iter().find(|r| r.package_name == "vendor/b"); assert!( conflict_result.is_some(), "vendor/b should prohibit vendor/a 2.0 via conflict" ); assert_eq!(conflict_result.unwrap().link_description, "conflicts"); } #[test] fn test_prohibits_no_issue() { // root requires A ^2.0; user asks "who prohibits A 2.5" // → root's constraint ^2.0 DOES match 2.5 → nobody prohibits it let packages = vec![ make_pkg("root/project", "ROOT", &[("vendor/a", "^2.0")], &[], true), make_pkg("vendor/a", "2.0.0", &[], &[], false), ]; let constraint = mozart_semver::VersionConstraint::parse("2.5.0").unwrap(); let needles = vec!["vendor/a".to_string()]; let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); assert!( results.is_empty(), "Nobody prohibits vendor/a 2.5 when root requires ^2.0" ); } #[test] fn test_print_table_empty() { let console = mozart_core::console::Console::new(0, false, false, false, false); print_table(&[], &console); } #[test] fn test_print_table_single() { let console = mozart_core::console::Console::new(0, false, false, false, false); let results = vec![DependencyResult { package_name: "vendor/a".to_string(), package_version: "1.0.0".to_string(), link_description: "requires".to_string(), link_target: "vendor/b".to_string(), link_constraint: "^2.0".to_string(), children: vec![], }]; print_table(&results, &console); } #[test] fn test_print_tree_empty() { let console = mozart_core::console::Console::new(0, false, false, false, false); print_tree(&[], 0, &console); } #[test] fn test_print_tree_nested() { let console = mozart_core::console::Console::new(0, false, false, false, false); let results = vec![DependencyResult { package_name: "vendor/a".to_string(), package_version: "1.0.0".to_string(), link_description: "requires".to_string(), link_target: "vendor/b".to_string(), link_constraint: "^2.0".to_string(), children: vec![DependencyResult { package_name: "root/project".to_string(), package_version: "ROOT".to_string(), link_description: "requires".to_string(), link_target: "vendor/a".to_string(), link_constraint: "^1.0".to_string(), children: vec![], }], }]; print_tree(&results, 0, &console); } }