2025-08-26 12:38:36 +02:00
use std ::collections ::{ HashMap , HashSet } ;
use std ::path ::PathBuf ;
use clap ::{ ArgAction , Parser , ValueEnum } ;
use miette ::{ Diagnostic , IntoDiagnostic , Result } ;
use thiserror ::Error ;
2025-08-26 14:53:31 +02:00
use tracing ::{ debug , info , warn } ;
2025-08-26 12:38:36 +02:00
use tracing_subscriber ::EnvFilter ;
use libips ::image ::Image ;
#[ derive(Parser, Debug) ]
2025-08-26 14:53:31 +02:00
#[ command(name = " pkgtree " , version, about = " Analyze IPS package dependency trees, detect cycles, and advise on failing installs " , long_about = None) ]
2025-08-26 12:38:36 +02:00
struct Cli {
/// Path to an IPS image (root containing var/pkg)
2025-08-26 14:53:31 +02:00
#[ arg(short = 'I', long = " image " , env = " IPS_IMAGE " ) ]
2025-08-26 12:38:36 +02:00
image_path : PathBuf ,
/// Publisher to analyze (default: all publishers in the image)
#[ arg(short = 'P', long) ]
publisher : Option < String > ,
/// Only analyze packages whose stem or FMRI contains this substring (case sensitive)
#[ arg(short = 'n', long) ]
package : Option < String > ,
2025-08-26 14:53:31 +02:00
/// Output format for graph mode
2025-08-26 12:38:36 +02:00
#[ arg(short = 'F', long = " format " , default_value_t = OutputFormat::Tree) ]
format : OutputFormat ,
/// Maximum depth to print for the tree (0 = unlimited)
#[ arg(short = 'd', long = " max-depth " , default_value_t = 0) ]
max_depth : usize ,
2025-08-26 14:53:31 +02:00
/// Detect and report dependency cycles across the analyzed set (graph mode)
2025-08-26 12:38:36 +02:00
#[ arg(short = 'c', long = " detect-cycles " , action = ArgAction::SetTrue) ]
detect_cycles : bool ,
2025-08-26 14:53:31 +02:00
/// Emit suggestions to break detected cycles (graph mode)
2025-08-26 12:38:36 +02:00
#[ arg(short = 's', long = " suggest " , action = ArgAction::SetTrue) ]
suggest : bool ,
2025-08-26 21:09:06 +02:00
/// Find packages whose dependencies reference missing stems (dangling)
#[ arg(long = " find-dangling " , action = ArgAction::SetTrue) ]
find_dangling : bool ,
2025-08-26 14:53:31 +02:00
/// Advise on an install for the given package stem (advisor mode)
#[ arg(long = " advise-install " ) ]
advise_install : Option < String > ,
/// Analyze a pkg6 solver error text file and suggest fixes (targeted mode)
#[ arg(long = " analyze-solver-error " ) ]
solver_error_file : Option < PathBuf > ,
/// Maximum recursion depth for advisor mode (default: 2)
#[ arg(long = " advice-depth " , default_value_t = 2) ]
advice_depth : usize ,
/// Maximum number of dependencies processed per package in advisor mode (0 = unlimited)
#[ arg(long = " advice-cap " , default_value_t = 400) ]
advice_cap : usize ,
2025-08-26 12:38:36 +02:00
/// Increase log verbosity (use multiple times)
#[ arg(short = 'v', long = " verbose " , action = ArgAction::Count) ]
verbose : u8 ,
}
#[ derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum) ]
enum OutputFormat {
Tree ,
Json ,
}
impl std ::fmt ::Display for OutputFormat {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
match self {
OutputFormat ::Tree = > write! ( f , " tree " ) ,
OutputFormat ::Json = > write! ( f , " json " ) ,
}
}
}
#[ derive(Error, Debug, Diagnostic) ]
2026-01-18 17:05:59 +01:00
#[ error( " pkgtree error: {0} " ) ]
2025-12-22 20:10:17 +01:00
#[ diagnostic(
code ( ips ::pkgtree_error ) ,
help ( " See logs with RUST_LOG=pkgtree=debug for more details. " )
) ]
2026-01-18 17:05:59 +01:00
struct PkgTreeError ( String ) ;
2025-08-26 12:38:36 +02:00
#[ derive(Debug, Clone) ]
struct Edge {
2025-12-22 20:10:17 +01:00
to : String , // target stem
dep_type : String , // dependency type (e.g., require, incorporate, optional, etc.)
2025-08-26 12:38:36 +02:00
}
#[ derive(Debug, Default, Clone) ]
struct Graph {
// stem -> edges
adj : HashMap < String , Vec < Edge > > ,
}
impl Graph {
fn add_edge ( & mut self , from : String , to : String , dep_type : String ) {
2025-12-22 20:10:17 +01:00
self . adj
. entry ( from )
. or_default ( )
. push ( Edge { to , dep_type } ) ;
2025-08-26 12:38:36 +02:00
}
fn stems ( & self ) -> impl Iterator < Item = & String > {
self . adj . keys ( )
}
}
#[ derive(Debug, Clone) ]
struct Cycle {
nodes : Vec < String > , // ordered stems forming the cycle, first == last for readability
edges : Vec < String > , // edge types along the cycle
}
fn main ( ) -> Result < ( ) > {
let cli = Cli ::parse ( ) ;
// Setup tracing
let env_filter = match cli . verbose {
0 = > EnvFilter ::from_default_env ( ) . add_directive ( " pkgtree=info " . parse ( ) . unwrap ( ) ) ,
1 = > EnvFilter ::from_default_env ( ) . add_directive ( " pkgtree=debug " . parse ( ) . unwrap ( ) ) ,
_ = > EnvFilter ::from_default_env ( ) . add_directive ( " pkgtree=trace " . parse ( ) . unwrap ( ) ) ,
} ;
tracing_subscriber ::fmt ( ) . with_env_filter ( env_filter ) . init ( ) ;
// Load image
2026-01-25 23:17:49 +01:00
let image = Image ::load ( & cli . image_path ) . map_err ( | e | {
PkgTreeError ( format! (
" Failed to load image at {:?}: {} " ,
cli . image_path , e
) )
} ) ? ;
2025-08-26 12:38:36 +02:00
2025-08-26 14:53:31 +02:00
// Targeted analysis of solver error file has top priority if provided
if let Some ( err_path ) = & cli . solver_error_file {
analyze_solver_error ( & image , cli . publisher . as_deref ( ) , err_path ) ? ;
return Ok ( ( ) ) ;
}
// Advisor mode has priority if requested
if let Some ( root ) = & cli . advise_install {
let mut ctx = AdviceContext ::new ( cli . publisher . clone ( ) , cli . advice_cap ) ;
run_advisor ( & image , & mut ctx , root , cli . advice_depth ) ? ;
return Ok ( ( ) ) ;
}
2025-08-26 21:09:06 +02:00
// Dangling dependency scan has priority over graph mode
if cli . find_dangling {
2025-12-22 20:10:17 +01:00
run_dangling_scan (
& image ,
cli . publisher . as_deref ( ) ,
cli . package . as_deref ( ) ,
cli . format ,
) ? ;
2025-08-26 21:09:06 +02:00
return Ok ( ( ) ) ;
}
2025-08-26 14:53:31 +02:00
// Graph mode
2025-08-26 12:38:36 +02:00
// Query catalog (filtered if --package provided)
let mut pkgs = if let Some ( ref needle ) = cli . package {
2025-12-22 20:10:17 +01:00
image
. query_catalog ( Some ( needle . as_str ( ) ) )
2026-01-18 17:05:59 +01:00
. map_err ( | e | PkgTreeError ( format! ( " Failed to query catalog: {} " , e ) ) ) ?
2025-08-26 12:38:36 +02:00
} else {
2026-01-25 23:17:49 +01:00
image
. query_catalog ( None )
. map_err ( | e | PkgTreeError ( format! ( " Failed to query catalog: {} " , e ) ) ) ?
2025-08-26 12:38:36 +02:00
} ;
// Filter by publisher if specified
if let Some ( pubname ) = & cli . publisher {
pkgs . retain ( | p | p . publisher = = * pubname ) ;
}
// Select starting set by package substring if requested
let filter_substr = cli . package . clone ( ) ;
// Build dependency graph from manifests
let mut graph = Graph ::default ( ) ;
for p in & pkgs {
// If filter is set and neither stem nor fmri string contains it, skip
if let Some ( ref needle ) = filter_substr {
let stem = p . fmri . stem ( ) . to_string ( ) ;
let fmri_str = p . fmri . to_string ( ) ;
if ! stem . contains ( needle ) & & ! fmri_str . contains ( needle ) {
continue ;
}
}
// Get manifest
match image . get_manifest_from_catalog ( & p . fmri ) {
Ok ( Some ( manifest ) ) = > {
let from_stem = p . fmri . stem ( ) . to_string ( ) ;
for dep in manifest . dependencies {
2025-08-26 14:53:31 +02:00
if dep . dependency_type ! = " require " & & dep . dependency_type ! = " incorporate " {
continue ;
}
2025-08-26 12:38:36 +02:00
if let Some ( dep_fmri ) = dep . fmri {
let to_stem = dep_fmri . stem ( ) . to_string ( ) ;
graph . add_edge ( from_stem . clone ( ) , to_stem , dep . dependency_type . clone ( ) ) ;
}
}
}
Ok ( None ) = > {
warn! ( fmri = % p . fmri . to_string ( ) , " Manifest not found in catalog " ) ;
}
Err ( err ) = > {
warn! ( error = % format! ( " {} " , err ) , fmri = % p . fmri . to_string ( ) , " Failed to get manifest from catalog " ) ;
}
}
}
// If no nodes were added (e.g., filter too narrow), try building graph for all packages to support cycle analysis
if graph . adj . is_empty ( ) & & filter_substr . is_some ( ) {
2025-12-22 20:10:17 +01:00
info! (
" No packages matched filter for dependency graph; analyzing full catalog for cycles/tree context. "
) ;
2025-08-26 12:38:36 +02:00
for p in & pkgs {
match image . get_manifest_from_catalog ( & p . fmri ) {
Ok ( Some ( manifest ) ) = > {
let from_stem = p . fmri . stem ( ) . to_string ( ) ;
for dep in manifest . dependencies {
2025-12-22 20:10:17 +01:00
if dep . dependency_type ! = " require " & & dep . dependency_type ! = " incorporate "
{
2025-08-26 14:53:31 +02:00
continue ;
}
2025-08-26 12:38:36 +02:00
if let Some ( dep_fmri ) = dep . fmri {
let to_stem = dep_fmri . stem ( ) . to_string ( ) ;
graph . add_edge ( from_stem . clone ( ) , to_stem , dep . dependency_type . clone ( ) ) ;
}
}
}
_ = > { }
}
}
}
// Determine roots for tree printing
let roots : Vec < String > = if let Some ( ref needle ) = filter_substr {
let mut r = HashSet ::new ( ) ;
for k in graph . adj . keys ( ) {
2025-12-22 20:10:17 +01:00
if k . contains ( needle ) {
r . insert ( k . clone ( ) ) ;
}
2025-08-26 12:38:36 +02:00
}
r . into_iter ( ) . collect ( )
} else {
graph . adj . keys ( ) . cloned ( ) . collect ( )
} ;
// Optionally detect cycles
let mut cycles : Vec < Cycle > = Vec ::new ( ) ;
if cli . detect_cycles {
cycles = detect_cycles ( & graph ) ;
}
match cli . format {
OutputFormat ::Tree = > {
print_trees ( & graph , & roots , cli . max_depth ) ;
if cli . detect_cycles {
print_cycles ( & cycles ) ;
if cli . suggest {
print_suggestions ( & cycles , & graph ) ;
}
}
}
OutputFormat ::Json = > {
use serde ::Serialize ;
#[ derive(Serialize) ]
2025-12-22 20:10:17 +01:00
struct JsonEdge {
from : String ,
to : String ,
dep_type : String ,
}
2025-08-26 12:38:36 +02:00
#[ derive(Serialize) ]
2025-12-22 20:10:17 +01:00
struct JsonCycle {
nodes : Vec < String > ,
edges : Vec < String > ,
}
2025-08-26 12:38:36 +02:00
#[ derive(Serialize) ]
2025-12-22 20:10:17 +01:00
struct Payload {
edges : Vec < JsonEdge > ,
cycles : Vec < JsonCycle > ,
}
2025-08-26 12:38:36 +02:00
let mut edges = Vec ::new ( ) ;
for ( from , es ) in & graph . adj {
2025-12-22 20:10:17 +01:00
for e in es {
edges . push ( JsonEdge {
from : from . clone ( ) ,
to : e . to . clone ( ) ,
dep_type : e . dep_type . clone ( ) ,
} ) ;
}
2025-08-26 12:38:36 +02:00
}
2025-12-22 20:10:17 +01:00
let cycles_json = cycles
. iter ( )
. map ( | c | JsonCycle {
nodes : c . nodes . clone ( ) ,
edges : c . edges . clone ( ) ,
} )
. collect ( ) ;
let payload = Payload {
edges ,
cycles : cycles_json ,
} ;
println! (
" {} " ,
serde_json ::to_string_pretty ( & payload ) . into_diagnostic ( ) ?
) ;
2025-08-26 12:38:36 +02:00
}
}
Ok ( ( ) )
}
2025-08-26 14:53:31 +02:00
// ---------- Advisor mode ----------
#[ derive(Debug, Clone) ]
struct DepConstraint {
release : Option < String > ,
branch : Option < String > ,
}
#[ derive(Debug, Clone) ]
struct AdviceIssue {
path : Vec < String > , // path from root to the missing dependency stem
stem : String , // the missing stem
constraint : DepConstraint ,
2025-12-22 20:10:17 +01:00
details : String , // human description
2025-08-26 14:53:31 +02:00
}
#[ derive(Default) ]
struct AdviceContext {
publisher : Option < String > ,
advice_cap : usize ,
// caches
catalog_cache : HashMap < String , Vec < ( String , libips ::fmri ::Fmri ) > > , // stem -> [(publisher, fmri)]
manifest_cache : HashMap < String , libips ::actions ::Manifest > , // fmri string -> manifest
lock_cache : HashMap < String , Option < String > > , // stem -> release lock
2025-12-22 20:10:17 +01:00
candidate_cache : HashMap <
( String , Option < String > , Option < String > , Option < String > ) ,
Option < libips ::fmri ::Fmri > ,
> , // (stem, rel, branch, publisher)
2025-08-26 14:53:31 +02:00
}
impl AdviceContext {
fn new ( publisher : Option < String > , advice_cap : usize ) -> Self {
2025-12-22 20:10:17 +01:00
AdviceContext {
publisher ,
advice_cap ,
.. Default ::default ( )
}
2025-08-26 14:53:31 +02:00
}
}
2025-12-22 20:10:17 +01:00
fn run_advisor (
image : & Image ,
ctx : & mut AdviceContext ,
root_stem : & str ,
max_depth : usize ,
) -> Result < ( ) > {
2025-08-26 14:53:31 +02:00
info! ( " Advisor analyzing installability for root: {} " , root_stem ) ;
// Find best candidate for root
let root_fmri = match find_best_candidate ( image , ctx , root_stem , None , None ) {
Ok ( Some ( fmri ) ) = > fmri ,
Ok ( None ) = > {
2025-12-22 20:10:17 +01:00
println! (
" No candidates found for root package '{}'. \n - Suggestion: run 'pkg6 refresh' to update catalogs. \n - Ensure publisher{} contains the package. " ,
root_stem ,
ctx . publisher
. as_ref ( )
. map ( | p | format! ( " ' {} ') " , p ) )
. unwrap_or_else ( | | " " . to_string ( ) )
) ;
2025-08-26 14:53:31 +02:00
return Ok ( ( ) ) ;
}
Err ( e ) = > return Err ( e ) ,
} ;
debug! ( " Chosen root FMRI: {} " , root_fmri . to_string ( ) ) ;
// Traverse dependencies up to depth and collect issues
let mut issues : Vec < AdviceIssue > = Vec ::new ( ) ;
let mut seen : HashSet < String > = HashSet ::new ( ) ;
let mut path : Vec < String > = vec! [ root_stem . to_string ( ) ] ;
2025-12-22 20:10:17 +01:00
advise_recursive (
image ,
ctx ,
& root_fmri ,
& mut path ,
1 ,
max_depth ,
& mut seen ,
& mut issues ,
) ? ;
2025-08-26 14:53:31 +02:00
// Print summary
if issues . is_empty ( ) {
2025-12-22 20:10:17 +01:00
println! (
" No immediate missing dependencies detected up to depth {} for root '{}'. \n If installs still fail, try running with higher --advice-depth or check solver logs. " ,
max_depth , root_stem
) ;
2025-08-26 14:53:31 +02:00
} else {
println! ( " Found {} installability issue(s): " , issues . len ( ) ) ;
for ( i , iss ) in issues . iter ( ) . enumerate ( ) {
let constraint_str = format! (
" {}{} " ,
2025-12-22 20:10:17 +01:00
iss . constraint
. release
. as_ref ( )
. map ( | r | format! ( " release= {} " , r ) )
. unwrap_or_default ( ) ,
iss . constraint
. branch
. as_ref ( )
. map ( | b | format! ( " branch= {} " , b ) )
. unwrap_or_default ( ) ,
)
. trim ( )
. to_string ( ) ;
println! (
" {}. {} \n - Path: {} \n - Constraint: {} \n - Details: {} " ,
2025-08-26 14:53:31 +02:00
i + 1 ,
format! ( " No viable candidates for ' {} ' " , iss . stem ) ,
iss . path . join ( " -> " ) ,
2025-12-22 20:10:17 +01:00
if constraint_str . is_empty ( ) {
" <none> " . to_string ( )
} else {
constraint_str
} ,
2025-08-26 14:53:31 +02:00
iss . details ,
) ;
// Suggestions
println! ( " - Suggestions: " ) ;
2025-12-22 20:10:17 +01:00
println! (
" • Add or publish a matching package for '{}'{}{}. " ,
2025-08-26 14:53:31 +02:00
iss . stem ,
2025-12-22 20:10:17 +01:00
iss . constraint
. release
. as_ref ( )
. map ( | r | format! ( " (release= {} ) " , r ) )
. unwrap_or_default ( ) ,
iss . constraint
. branch
. as_ref ( )
. map ( | b | format! ( " (branch= {} ) " , b ) )
. unwrap_or_default ( )
) ;
println! (
" • Alternatively, relax the dependency constraint in the requiring package to match available releases. "
) ;
if let Some ( lock ) = get_incorporated_release_cached ( image , ctx , & iss . stem )
. ok ( )
. flatten ( )
{
println! (
" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency. " ,
iss . stem , lock
) ;
2025-08-26 14:53:31 +02:00
}
println! ( " • Ensure catalogs are up to date: 'pkg6 refresh'. " ) ;
}
}
Ok ( ( ) )
}
fn advise_recursive (
image : & Image ,
ctx : & mut AdviceContext ,
fmri : & libips ::fmri ::Fmri ,
path : & mut Vec < String > ,
depth : usize ,
max_depth : usize ,
seen : & mut HashSet < String > ,
issues : & mut Vec < AdviceIssue > ,
) -> Result < ( ) > {
2025-12-22 20:10:17 +01:00
if max_depth ! = 0 & & depth > max_depth {
return Ok ( ( ) ) ;
}
2025-08-26 14:53:31 +02:00
// Load manifest of the current FMRI (cached)
let manifest = get_manifest_cached ( image , ctx , fmri ) ? ;
let mut processed = 0 usize ;
let mut constrained = Vec ::new ( ) ;
let mut unconstrained = Vec ::new ( ) ;
for dep in manifest . dependencies {
2025-12-22 20:10:17 +01:00
if dep . dependency_type ! = " require " & & dep . dependency_type ! = " incorporate " {
continue ;
}
2025-08-26 14:53:31 +02:00
let has_fmri = dep . fmri . is_some ( ) ;
2025-12-22 20:10:17 +01:00
if ! has_fmri {
continue ;
}
2025-08-26 14:53:31 +02:00
let c = extract_constraint ( & dep . optional ) ;
2025-12-22 20:10:17 +01:00
if c . release . is_some ( ) | | c . branch . is_some ( ) {
constrained . push ( ( dep , c ) ) ;
} else {
unconstrained . push ( ( dep , c ) ) ;
}
2025-08-26 14:53:31 +02:00
}
for ( dep , constraint ) in constrained . into_iter ( ) . chain ( unconstrained . into_iter ( ) ) {
if ctx . advice_cap ! = 0 & & processed > = ctx . advice_cap {
2025-12-22 20:10:17 +01:00
debug! (
" Dependency processing for {} truncated at cap {} " ,
fmri . stem ( ) ,
ctx . advice_cap
) ;
2025-08-26 14:53:31 +02:00
break ;
}
processed + = 1 ;
let dep_stem = dep . fmri . unwrap ( ) . stem ( ) . to_string ( ) ;
2025-12-22 20:10:17 +01:00
debug! (
" Checking dependency to '{}' with constraint {:?} " ,
dep_stem ,
( & constraint . release , & constraint . branch )
) ;
match find_best_candidate (
image ,
ctx ,
& dep_stem ,
constraint . release . as_deref ( ) ,
constraint . branch . as_deref ( ) ,
) ? {
2025-08-26 14:53:31 +02:00
Some ( next_fmri ) = > {
// Continue recursion if not seen and depth allows
if ! seen . contains ( & dep_stem ) {
seen . insert ( dep_stem . clone ( ) ) ;
path . push ( dep_stem . clone ( ) ) ;
2025-12-22 20:10:17 +01:00
advise_recursive (
image ,
ctx ,
& next_fmri ,
path ,
depth + 1 ,
max_depth ,
seen ,
issues ,
) ? ;
2025-08-26 14:53:31 +02:00
path . pop ( ) ;
}
}
None = > {
let details = build_missing_detail ( image , ctx , & dep_stem , & constraint ) ;
issues . push ( AdviceIssue {
path : path . clone ( ) ,
stem : dep_stem . clone ( ) ,
constraint : constraint . clone ( ) ,
details ,
} ) ;
}
}
}
Ok ( ( ) )
}
fn extract_constraint ( optional : & [ libips ::actions ::Property ] ) -> DepConstraint {
let mut release : Option < String > = None ;
let mut branch : Option < String > = None ;
for p in optional {
match p . key . as_str ( ) {
" release " = > release = Some ( p . value . clone ( ) ) ,
" branch " = > branch = Some ( p . value . clone ( ) ) ,
_ = > { }
}
}
DepConstraint { release , branch }
}
2025-12-22 20:10:17 +01:00
fn build_missing_detail (
image : & Image ,
ctx : & mut AdviceContext ,
stem : & str ,
constraint : & DepConstraint ,
) -> String {
2025-08-26 14:53:31 +02:00
// List available releases/branches for informational purposes
let mut available : Vec < String > = Vec ::new ( ) ;
if let Ok ( list ) = query_catalog_cached_mut ( image , ctx , stem ) {
for ( pubname , fmri ) in list {
2025-12-22 20:10:17 +01:00
if let Some ( ref pfilter ) = ctx . publisher {
if & pubname ! = pfilter {
continue ;
}
}
if fmri . stem ( ) ! = stem {
continue ;
}
2025-08-26 14:53:31 +02:00
let ver = fmri . version ( ) ;
2025-12-22 20:10:17 +01:00
if ver . is_empty ( ) {
continue ;
}
2025-08-26 14:53:31 +02:00
available . push ( ver ) ;
}
}
let mut available : Vec < String > = available . into_iter ( ) . collect ( ) ;
available . sort ( ) ;
available . dedup ( ) ;
let available_str = if available . is_empty ( ) {
" <none> " . to_string ( )
} else {
available . join ( " , " )
} ;
2025-12-22 20:10:17 +01:00
let lock = get_incorporated_release_cached ( image , ctx , stem )
. ok ( )
. flatten ( ) ;
2025-08-26 14:53:31 +02:00
match ( & constraint . release , & constraint . branch , lock ) {
2025-12-22 20:10:17 +01:00
( Some ( r ) , Some ( b ) , Some ( lr ) ) = > format! (
" Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {} " ,
r , b , lr , available_str
) ,
( Some ( r ) , Some ( b ) , None ) = > format! (
" Required release={}, branch={} not found. Available versions: {} " ,
r , b , available_str
) ,
( Some ( r ) , None , Some ( lr ) ) = > format! (
" Required release={} not found. Incorporation lock release={} present. Available versions: {} " ,
r , lr , available_str
) ,
( Some ( r ) , None , None ) = > format! (
" Required release={} not found. Available versions: {} " ,
r , available_str
) ,
( None , Some ( b ) , Some ( lr ) ) = > format! (
" Required branch={} not found. Incorporation lock release={} present. Available versions: {} " ,
b , lr , available_str
) ,
( None , Some ( b ) , None ) = > format! (
" Required branch={} not found. Available versions: {} " ,
b , available_str
) ,
( None , None , Some ( lr ) ) = > format! (
" No candidates matched. Incorporation lock release={} present. Available versions: {} " ,
lr , available_str
) ,
( None , None , None ) = > format! (
" No candidates matched. Available versions: {} " ,
available_str
) ,
2025-08-26 14:53:31 +02:00
}
}
fn find_best_candidate (
image : & Image ,
ctx : & mut AdviceContext ,
stem : & str ,
req_release : Option < & str > ,
req_branch : Option < & str > ,
) -> Result < Option < libips ::fmri ::Fmri > > {
let key = (
stem . to_string ( ) ,
req_release . map ( | s | s . to_string ( ) ) ,
req_branch . map ( | s | s . to_string ( ) ) ,
ctx . publisher . clone ( ) ,
) ;
if let Some ( cached ) = ctx . candidate_cache . get ( & key ) {
return Ok ( cached . clone ( ) ) ;
}
let mut candidates : Vec < ( String , libips ::fmri ::Fmri ) > = Vec ::new ( ) ;
// Prefer matching release from incorporation lock, unless explicit req_release provided
2025-12-22 20:10:17 +01:00
let lock_release = if req_release . is_none ( ) {
get_incorporated_release_cached ( image , ctx , stem )
. ok ( )
. flatten ( )
} else {
None
} ;
2025-08-26 14:53:31 +02:00
for ( pubf , pfmri ) in query_catalog_cached ( image , ctx , stem ) ? {
2025-12-22 20:10:17 +01:00
if let Some ( ref pfilter ) = ctx . publisher {
if & pubf ! = pfilter {
continue ;
}
}
if pfmri . stem ( ) ! = stem {
continue ;
}
2025-08-26 14:53:31 +02:00
let ver = pfmri . version ( ) ;
2025-12-22 20:10:17 +01:00
if ver . is_empty ( ) {
continue ;
}
2025-08-26 14:53:31 +02:00
// Parse version string to extract release and branch heuristically: release,branch-rest
let rel = version_release ( & ver ) ;
let br = version_branch ( & ver ) ;
if let Some ( req_r ) = req_release {
2025-12-22 20:10:17 +01:00
if Some ( req_r ) ! = rel . as_deref ( ) {
continue ;
}
2025-08-26 14:53:31 +02:00
} else if let Some ( lock_r ) = lock_release . as_deref ( ) {
2025-12-22 20:10:17 +01:00
if Some ( lock_r ) ! = rel . as_deref ( ) {
continue ;
}
2025-08-26 14:53:31 +02:00
}
2025-12-22 20:10:17 +01:00
if let Some ( req_b ) = req_branch {
if Some ( req_b ) ! = br . as_deref ( ) {
continue ;
}
}
2025-08-26 14:53:31 +02:00
candidates . push ( ( ver . clone ( ) , pfmri . clone ( ) ) ) ;
}
// Choose the lexicographically max version string (approximate latest)
candidates . sort_by ( | a , b | a . 0. cmp ( & b . 0 ) ) ;
let res = candidates . pop ( ) . map ( | x | x . 1 ) ;
ctx . candidate_cache . insert ( key , res . clone ( ) ) ;
Ok ( res )
}
fn version_release ( version : & str ) -> Option < String > {
// Format like: "1.35,5.11-2023.0.0.0:TS" => release before comma
version . split_once ( ',' ) . map ( | ( rel , _ ) | rel . to_string ( ) )
}
fn version_branch ( version : & str ) -> Option < String > {
// Format like: "1.35,5.11-2023.0.0.0:TS" => branch between "," and "-"
if let Some ( ( _ , rest ) ) = version . split_once ( ',' ) {
return rest . split_once ( '-' ) . map ( | ( b , _ ) | b . to_string ( ) ) ;
}
None
}
// ---------- Caching helpers ----------
fn query_catalog_cached (
image : & Image ,
ctx : & AdviceContext ,
stem : & str ,
) -> Result < Vec < ( String , libips ::fmri ::Fmri ) > > {
if let Some ( v ) = ctx . catalog_cache . get ( stem ) {
return Ok ( v . clone ( ) ) ;
}
// We don't have mutable borrow on ctx here; clone and return, caller will populate cache through a mutable wrapper.
// To keep code simple, provide a small wrapper that fills the cache when needed.
// We'll implement a separate function that has mutable ctx.
2025-12-22 20:10:17 +01:00
let mut tmp_ctx = AdviceContext {
catalog_cache : ctx . catalog_cache . clone ( ) ,
.. Default ::default ( )
} ;
2025-08-26 14:53:31 +02:00
query_catalog_cached_mut ( image , & mut tmp_ctx , stem )
}
fn query_catalog_cached_mut (
image : & Image ,
ctx : & mut AdviceContext ,
stem : & str ,
) -> Result < Vec < ( String , libips ::fmri ::Fmri ) > > {
if let Some ( v ) = ctx . catalog_cache . get ( stem ) {
return Ok ( v . clone ( ) ) ;
}
let mut out = Vec ::new ( ) ;
2026-01-25 23:17:49 +01:00
for p in image
. query_catalog ( Some ( stem ) )
. map_err ( | e | PkgTreeError ( format! ( " Failed to query catalog for {} : {} " , stem , e ) ) ) ?
{
2025-08-26 14:53:31 +02:00
out . push ( ( p . publisher , p . fmri ) ) ;
}
ctx . catalog_cache . insert ( stem . to_string ( ) , out . clone ( ) ) ;
Ok ( out )
}
fn get_manifest_cached (
image : & Image ,
ctx : & mut AdviceContext ,
fmri : & libips ::fmri ::Fmri ,
) -> Result < libips ::actions ::Manifest > {
let key = fmri . to_string ( ) ;
if let Some ( m ) = ctx . manifest_cache . get ( & key ) {
return Ok ( m . clone ( ) ) ;
}
2026-01-25 23:17:49 +01:00
let manifest_opt = image . get_manifest_from_catalog ( fmri ) . map_err ( | e | {
PkgTreeError ( format! (
" Failed to load manifest for {}: {} " ,
fmri . to_string ( ) ,
e
) )
} ) ? ;
2025-08-26 14:53:31 +02:00
let manifest = manifest_opt . unwrap_or_else ( | | libips ::actions ::Manifest ::new ( ) ) ;
ctx . manifest_cache . insert ( key , manifest . clone ( ) ) ;
Ok ( manifest )
}
fn get_incorporated_release_cached (
image : & Image ,
ctx : & mut AdviceContext ,
stem : & str ,
) -> Result < Option < String > > {
2025-12-22 20:10:17 +01:00
if let Some ( v ) = ctx . lock_cache . get ( stem ) {
return Ok ( v . clone ( ) ) ;
}
2025-08-26 14:53:31 +02:00
let v = image . get_incorporated_release ( stem ) ? ;
ctx . lock_cache . insert ( stem . to_string ( ) , v . clone ( ) ) ;
Ok ( v )
}
// ---------- Graph mode helpers ----------
2025-08-26 12:38:36 +02:00
fn print_trees ( graph : & Graph , roots : & [ String ] , max_depth : usize ) {
// Print a tree for each root
let mut printed = HashSet ::new ( ) ;
for r in roots {
2025-12-22 20:10:17 +01:00
if printed . contains ( r ) {
continue ;
}
2025-08-26 12:38:36 +02:00
printed . insert ( r . clone ( ) ) ;
println! ( " {} " , r ) ;
let mut path = Vec ::new ( ) ;
let mut seen = HashSet ::new ( ) ;
print_tree_rec ( graph , r , 1 , max_depth , & mut path , & mut seen ) ;
println! ( " " ) ;
}
}
fn print_tree_rec (
graph : & Graph ,
node : & str ,
depth : usize ,
max_depth : usize ,
path : & mut Vec < String > ,
2025-08-26 14:53:31 +02:00
_seen : & mut HashSet < String > ,
2025-08-26 12:38:36 +02:00
) {
2025-12-22 20:10:17 +01:00
if max_depth ! = 0 & & depth > max_depth {
return ;
}
2025-08-26 12:38:36 +02:00
path . push ( node . to_string ( ) ) ;
if let Some ( edges ) = graph . adj . get ( node ) {
for e in edges {
let last = if path . contains ( & e . to ) { " (cycle) " } else { " " } ;
println! ( " {} └─ {} [ {} ] {} " , " " . repeat ( depth ) , e . to , e . dep_type , last ) ;
if ! path . contains ( & e . to ) {
2025-08-26 14:53:31 +02:00
print_tree_rec ( graph , & e . to , depth + 1 , max_depth , path , _seen ) ;
2025-08-26 12:38:36 +02:00
}
}
}
path . pop ( ) ;
}
fn detect_cycles ( graph : & Graph ) -> Vec < Cycle > {
let mut visited : HashSet < String > = HashSet ::new ( ) ;
let mut stack : Vec < String > = Vec ::new ( ) ;
let mut cycles = Vec ::new ( ) ;
for node in graph . stems ( ) . cloned ( ) . collect ::< Vec < _ > > ( ) {
if ! visited . contains ( & node ) {
dfs_cycles ( graph , & node , & mut visited , & mut stack , & mut cycles ) ;
}
}
dedup_cycles ( cycles )
}
fn dfs_cycles (
graph : & Graph ,
node : & str ,
visited : & mut HashSet < String > ,
stack : & mut Vec < String > ,
cycles : & mut Vec < Cycle > ,
) {
visited . insert ( node . to_string ( ) ) ;
stack . push ( node . to_string ( ) ) ;
if let Some ( edges ) = graph . adj . get ( node ) {
for e in edges {
let to = & e . to ;
if let Some ( pos ) = stack . iter ( ) . position ( | n | n = = to ) {
// Found a cycle: stack[pos..] -> to
let mut cycle_nodes = stack [ pos .. ] . to_vec ( ) ;
cycle_nodes . push ( to . clone ( ) ) ;
let mut cycle_edges = Vec ::new ( ) ;
2025-08-26 14:53:31 +02:00
for i in pos .. stack . len ( ) {
2025-08-26 12:38:36 +02:00
let from = & stack [ i ] ;
2025-12-22 20:10:17 +01:00
let to2 = if i + 1 < stack . len ( ) {
& stack [ i + 1 ]
} else {
to
} ;
2025-08-26 12:38:36 +02:00
if let Some ( es2 ) = graph . adj . get ( from ) {
if let Some ( edge ) = es2 . iter ( ) . find ( | ed | & ed . to = = to2 ) {
cycle_edges . push ( edge . dep_type . clone ( ) ) ;
} else {
cycle_edges . push ( " unknown " . to_string ( ) ) ;
}
}
}
2025-12-22 20:10:17 +01:00
cycles . push ( Cycle {
nodes : cycle_nodes ,
edges : cycle_edges ,
} ) ;
2025-08-26 12:38:36 +02:00
} else if ! visited . contains ( to ) {
dfs_cycles ( graph , to , visited , stack , cycles ) ;
}
}
}
stack . pop ( ) ;
}
fn dedup_cycles ( mut cycles : Vec < Cycle > ) -> Vec < Cycle > {
// Normalize cycles so that smallest node lexicographically is first, and ensure start==end
for c in cycles . iter_mut ( ) {
if c . nodes . first ( ) ! = c . nodes . last ( ) & & ! c . nodes . is_empty ( ) {
c . nodes . push ( c . nodes . first ( ) . unwrap ( ) . clone ( ) ) ;
}
// rotate to minimal node position (excluding the duplicate last element when comparing)
if c . nodes . len ( ) > 1 {
2025-12-22 20:10:17 +01:00
let inner = & c . nodes [ .. c . nodes . len ( ) - 1 ] ;
2025-08-26 12:38:36 +02:00
if let Some ( ( min_idx , _ ) ) = inner . iter ( ) . enumerate ( ) . min_by_key ( | ( _ , n ) | * n ) {
c . nodes . rotate_left ( min_idx ) ;
c . edges . rotate_left ( min_idx ) ;
}
}
}
// Deduplicate by string key
let mut seen = HashSet ::new ( ) ;
cycles . retain ( | c | {
let key = c . nodes . join ( " -> " ) ;
2025-12-22 20:10:17 +01:00
if seen . contains ( & key ) {
false
} else {
seen . insert ( key ) ;
true
}
2025-08-26 12:38:36 +02:00
} ) ;
cycles
}
fn print_cycles ( cycles : & [ Cycle ] ) {
if cycles . is_empty ( ) {
println! ( " No dependency cycles detected. " ) ;
return ;
}
println! ( " Detected {} cycle(s): " , cycles . len ( ) ) ;
for ( i , c ) in cycles . iter ( ) . enumerate ( ) {
println! ( " {} . {} " , i + 1 , c . nodes . join ( " -> " ) ) ;
}
}
fn print_suggestions ( cycles : & [ Cycle ] , graph : & Graph ) {
2025-12-22 20:10:17 +01:00
if cycles . is_empty ( ) {
return ;
}
2025-08-26 12:38:36 +02:00
println! ( " \n Suggestions to break cycles (heuristic): " ) ;
for ( i , c ) in cycles . iter ( ) . enumerate ( ) {
// Prefer breaking an 'incorporate' edge if present, otherwise any edge
let mut suggested : Option < ( String , String ) > = None ; // (from, to)
' outer : for w in c . nodes . windows ( 2 ) {
let from = & w [ 0 ] ;
let to = & w [ 1 ] ;
if let Some ( es ) = graph . adj . get ( from ) {
for e in es {
if & e . to = = to {
2025-12-22 20:10:17 +01:00
if e . dep_type = = " incorporate " {
suggested = Some ( ( from . clone ( ) , to . clone ( ) ) ) ;
break 'outer ;
}
if suggested . is_none ( ) {
suggested = Some ( ( from . clone ( ) , to . clone ( ) ) ) ;
}
2025-08-26 12:38:36 +02:00
}
}
}
}
if let Some ( ( from , to ) ) = suggested {
2025-12-22 20:10:17 +01:00
println! (
" {}. Consider relaxing/removing edge {} -> {} (preferably if it's an incorporation). " ,
i + 1 ,
from ,
to
) ;
2025-08-26 12:38:36 +02:00
} else {
2025-12-22 20:10:17 +01:00
println! (
" {}. Consider relaxing one edge along the cycle: {} " ,
i + 1 ,
c . nodes . join ( " -> " )
) ;
2025-08-26 12:38:36 +02:00
}
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn detects_simple_cycle ( ) {
let mut g = Graph ::default ( ) ;
g . add_edge ( " A " . to_string ( ) , " B " . to_string ( ) , " require " . to_string ( ) ) ;
g . add_edge ( " B " . to_string ( ) , " C " . to_string ( ) , " require " . to_string ( ) ) ;
g . add_edge ( " C " . to_string ( ) , " A " . to_string ( ) , " incorporate " . to_string ( ) ) ;
let cycles = detect_cycles ( & g ) ;
assert! ( ! cycles . is_empty ( ) ) ;
}
2025-08-26 14:53:31 +02:00
#[ test ]
fn version_parsing_helpers ( ) {
let v = " 1.35,5.11-2023.0.0.0:20230723T105730Z " ;
assert_eq! ( version_release ( v ) . as_deref ( ) , Some ( " 1.35 " ) ) ;
assert_eq! ( version_branch ( v ) . as_deref ( ) , Some ( " 5.11 " ) ) ;
}
}
2025-08-26 21:09:06 +02:00
// ---------- Dangling dependency scan ----------
fn run_dangling_scan (
image : & Image ,
publisher : Option < & str > ,
package_filter : Option < & str > ,
format : OutputFormat ,
) -> Result < ( ) > {
// Query full catalog once
2026-01-25 23:17:49 +01:00
let mut pkgs = image
. query_catalog ( None )
. map_err ( | e | PkgTreeError ( format! ( " Failed to query catalog: {} " , e ) ) ) ? ;
2025-08-26 21:09:06 +02:00
// Build set of available non-obsolete stems AND an index of available (release, branch) pairs per stem,
// honoring publisher filter
let mut available_stems : HashSet < String > = HashSet ::new ( ) ;
let mut available_index : HashMap < String , Vec < ( String , Option < String > ) > > = HashMap ::new ( ) ;
for p in & pkgs {
if let Some ( pubf ) = publisher {
2025-12-22 20:10:17 +01:00
if p . publisher ! = pubf {
continue ;
}
}
if p . obsolete {
continue ;
2025-08-26 21:09:06 +02:00
}
let stem = p . fmri . stem ( ) . to_string ( ) ;
available_stems . insert ( stem . clone ( ) ) ;
let ver = p . fmri . version ( ) ;
if ! ver . is_empty ( ) {
if let Some ( rel ) = version_release ( & ver ) {
let br = version_branch ( & ver ) ;
available_index . entry ( stem ) . or_default ( ) . push ( ( rel , br ) ) ;
}
}
}
// Filter the list of requiring packages we'll scan
if let Some ( pubf ) = publisher {
pkgs . retain ( | p | p . publisher = = pubf ) ;
}
pkgs . retain ( | p | ! p . obsolete ) ;
if let Some ( needle ) = package_filter {
pkgs . retain ( | p | p . fmri . stem ( ) . contains ( needle ) | | p . fmri . to_string ( ) . contains ( needle ) ) ;
}
// Map of requiring package fmri string -> Vec<missing_stems>
let mut dangling : HashMap < String , Vec < String > > = HashMap ::new ( ) ;
for p in & pkgs {
let fmri = & p . fmri ;
match image . get_manifest_from_catalog ( fmri ) {
Ok ( Some ( man ) ) = > {
let mut missing_for_pkg : Vec < String > = Vec ::new ( ) ;
for dep in man . dependencies {
2025-12-22 20:10:17 +01:00
if dep . dependency_type ! = " require " & & dep . dependency_type ! = " incorporate " {
continue ;
}
let Some ( df ) = dep . fmri else {
continue ;
} ;
2025-08-26 21:09:06 +02:00
let stem = df . stem ( ) . to_string ( ) ;
// Extract version/branch constraints if any (from optional properties)
let mut c = extract_constraint ( & dep . optional ) ;
// Also merge constraints from the dependency FMRI's version string if not provided in optional
let df_ver_str = df . version ( ) ;
if ! df_ver_str . is_empty ( ) {
if c . release . is_none ( ) {
c . release = version_release ( & df_ver_str ) ;
}
if c . branch . is_none ( ) {
c . branch = version_branch ( & df_ver_str ) ;
}
}
// Helper to check availability against constraints
let satisfies = | stem : & str , rel : Option < & str > , br : Option < & str > | -> bool {
if let Some ( list ) = available_index . get ( stem ) {
if let ( Some ( rreq ) , Some ( breq ) ) = ( rel , br ) {
2025-12-22 20:10:17 +01:00
return list
. iter ( )
. any ( | ( r , b ) | r = = rreq & & b . as_deref ( ) = = Some ( breq ) ) ;
2025-08-26 21:09:06 +02:00
} else if let Some ( rreq ) = rel {
return list . iter ( ) . any ( | ( r , _ ) | r = = rreq ) ;
} else if let Some ( breq ) = br {
return list . iter ( ) . any ( | ( _ , b ) | b . as_deref ( ) = = Some ( breq ) ) ;
} else {
return true ; // no constraint: stem existing already confirmed elsewhere
}
}
false
} ;
let mut mark_missing : Option < String > = None ;
if ! available_stems . contains ( & stem ) {
mark_missing = Some ( stem . clone ( ) ) ;
} else if c . release . is_some ( ) | | c . branch . is_some ( ) {
if ! satisfies ( & stem , c . release . as_deref ( ) , c . branch . as_deref ( ) ) {
// Include constraint context in output for maintainers
let mut ctx = String ::new ( ) ;
2025-12-22 20:10:17 +01:00
if let Some ( r ) = & c . release {
ctx . push_str ( & format! ( " release= {} " , r ) ) ;
}
if let Some ( b ) = & c . branch {
ctx . push_str ( & format! ( " branch= {} " , b ) ) ;
}
2025-08-26 21:09:06 +02:00
let ctx = ctx . trim ( ) . to_string ( ) ;
2025-12-22 20:10:17 +01:00
if ctx . is_empty ( ) {
mark_missing = Some ( stem . clone ( ) ) ;
} else {
mark_missing = Some ( format! ( " {} [required {} ] " , stem , ctx ) ) ;
}
2025-08-26 21:09:06 +02:00
}
}
2025-12-22 20:10:17 +01:00
if let Some ( m ) = mark_missing {
missing_for_pkg . push ( m ) ;
}
2025-08-26 21:09:06 +02:00
}
if ! missing_for_pkg . is_empty ( ) {
missing_for_pkg . sort ( ) ;
missing_for_pkg . dedup ( ) ;
dangling . insert ( fmri . to_string ( ) , missing_for_pkg ) ;
}
}
Ok ( None ) = > {
warn! ( pkg = % fmri . to_string ( ) , " Manifest not found in catalog while scanning dangling deps " ) ;
}
Err ( e ) = > {
warn! ( pkg = % fmri . to_string ( ) , error = % format! ( " {} " , e ) , " Failed to read manifest while scanning dangling deps " ) ;
}
}
}
// Output
match format {
OutputFormat ::Tree = > {
if dangling . is_empty ( ) {
println! ( " No dangling dependencies detected. " ) ;
} else {
2025-12-22 20:10:17 +01:00
println! (
" Found {} package(s) with dangling dependencies: " ,
dangling . len ( )
) ;
2025-08-26 21:09:06 +02:00
let mut keys : Vec < String > = dangling . keys ( ) . cloned ( ) . collect ( ) ;
keys . sort ( ) ;
for k in keys {
println! ( " - {} : " , k ) ;
if let Some ( list ) = dangling . get ( & k ) {
2025-12-22 20:10:17 +01:00
for m in list {
println! ( " • {} " , m ) ;
}
2025-08-26 21:09:06 +02:00
}
}
}
}
OutputFormat ::Json = > {
use serde ::Serialize ;
#[ derive(Serialize) ]
2025-12-22 20:10:17 +01:00
struct DanglingJson {
package_fmri : String ,
missing_stems : Vec < String > ,
}
2025-08-26 21:09:06 +02:00
let mut out : Vec < DanglingJson > = Vec ::new ( ) ;
for ( pkg , miss ) in dangling . into_iter ( ) {
2025-12-22 20:10:17 +01:00
out . push ( DanglingJson {
package_fmri : pkg ,
missing_stems : miss ,
} ) ;
2025-08-26 21:09:06 +02:00
}
out . sort_by ( | a , b | a . package_fmri . cmp ( & b . package_fmri ) ) ;
println! ( " {} " , serde_json ::to_string_pretty ( & out ) . into_diagnostic ( ) ? ) ;
}
}
Ok ( ( ) )
}
2025-08-26 14:53:31 +02:00
// ---------- Targeted analysis: parse pkg6 solver error text ----------
fn analyze_solver_error ( image : & Image , publisher : Option < & str > , err_path : & PathBuf ) -> Result < ( ) > {
2026-01-25 23:17:49 +01:00
let text = std ::fs ::read_to_string ( err_path ) . map_err ( | e | {
PkgTreeError ( format! (
" Failed to read solver error file {:?}: {} " ,
err_path , e
) )
} ) ? ;
2025-08-26 14:53:31 +02:00
// Build a stack based on indentation before the tree bullet "└─".
let mut stack : Vec < String > = Vec ::new ( ) ;
let mut captured_path : Vec < String > = Vec ::new ( ) ;
let mut failing_leaf : Option < String > = None ;
for line in text . lines ( ) {
if let Some ( idx ) = line . find ( " └ " ) {
// Count spaces before the bullet to infer depth (~3 spaces per level in our output)
let indent = line [ .. idx ] . chars ( ) . filter ( | c | * c = = ' ' ) . count ( ) ;
let level = indent / 3 ; // heuristic
// Extract node text after "└─ "
let bullet = " └─ " ;
2025-12-22 20:10:17 +01:00
let start = match line . find ( bullet ) {
Some ( p ) = > p + bullet . len ( ) ,
None = > continue ,
} ;
2025-08-26 14:53:31 +02:00
let mut node_full = line [ start .. ] . trim ( ) . to_string ( ) ;
// Remove trailing diagnostic phrases for leaf line
if let Some ( pos ) = node_full . find ( " for which no candidates were found " ) {
node_full = node_full [ .. pos ] . trim ( ) . trim_end_matches ( ',' ) . to_string ( ) ;
}
2025-12-22 20:10:17 +01:00
if level > = stack . len ( ) {
stack . push ( node_full . clone ( ) ) ;
} else {
stack . truncate ( level ) ;
stack . push ( node_full . clone ( ) ) ;
}
2025-08-26 14:53:31 +02:00
if line . contains ( " for which no candidates were found " ) {
failing_leaf = Some ( node_full . clone ( ) ) ;
captured_path = stack . clone ( ) ;
break ;
}
}
}
if failing_leaf . is_none ( ) {
2025-12-22 20:10:17 +01:00
println! (
" Could not find a 'for which no candidates were found' leaf in the provided solver error file. "
) ;
2025-08-26 14:53:31 +02:00
return Ok ( ( ) ) ;
}
let leaf = failing_leaf . unwrap ( ) ;
// Extract stem and constraints from the leaf node text.
let ( stem , constraint ) = parse_leaf_node ( & leaf ) ;
// Prepare context and produce detailed suggestion
let mut ctx = AdviceContext ::new ( publisher . map ( | s | s . to_string ( ) ) , 0 ) ;
let details = build_missing_detail ( image , & mut ctx , & stem , & constraint ) ;
// Build a readable path using stems
let path_stems : Vec < String > = captured_path
. into_iter ( )
. map ( | n | stem_from_node ( & n ) )
. collect ( ) ;
println! ( " Found 1 installability issue (from solver error): " ) ;
let constraint_str = format! (
" {}{} " ,
2025-12-22 20:10:17 +01:00
constraint
. release
. as_ref ( )
. map ( | r | format! ( " release= {} " , r ) )
. unwrap_or_default ( ) ,
constraint
. branch
. as_ref ( )
. map ( | b | format! ( " branch= {} " , b ) )
. unwrap_or_default ( ) ,
)
. trim ( )
. to_string ( ) ;
println! (
" 1. No viable candidates for '{}' \n - Path: {} \n - Constraint: {} \n - Details: {} " ,
2025-08-26 14:53:31 +02:00
stem ,
path_stems . join ( " -> " ) ,
2025-12-22 20:10:17 +01:00
if constraint_str . is_empty ( ) {
" <none> " . to_string ( )
} else {
constraint_str
} ,
2025-08-26 14:53:31 +02:00
details ,
) ;
println! ( " - Suggestions: " ) ;
2025-12-22 20:10:17 +01:00
println! (
" • Add or publish a matching package for '{}'{}{}. " ,
2025-08-26 14:53:31 +02:00
stem ,
2025-12-22 20:10:17 +01:00
constraint
. release
. as_ref ( )
. map ( | r | format! ( " (release= {} ) " , r ) )
. unwrap_or_default ( ) ,
constraint
. branch
. as_ref ( )
. map ( | b | format! ( " (branch= {} ) " , b ) )
. unwrap_or_default ( )
) ;
println! (
" • Alternatively, relax the dependency constraint in the requiring package to match available releases. "
) ;
if let Some ( lock ) = get_incorporated_release_cached ( image , & mut ctx , & stem )
. ok ( )
. flatten ( )
{
println! (
" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency. " ,
stem , lock
) ;
2025-08-26 14:53:31 +02:00
}
println! ( " • Ensure catalogs are up to date: 'pkg6 refresh'. " ) ;
Ok ( ( ) )
}
fn stem_from_node ( node : & str ) -> String {
// Node may be like: "pkg://...@ver would require" or "archiver/gnu-tar branch=5.11, which ..." or just a stem
let first = node . split_whitespace ( ) . next ( ) . unwrap_or ( " " ) ;
if first . starts_with ( " pkg:// " ) {
2025-12-22 20:10:17 +01:00
if let Ok ( fmri ) = libips ::fmri ::Fmri ::parse ( first ) {
return fmri . stem ( ) . to_string ( ) ;
}
2025-08-26 14:53:31 +02:00
}
// If it contains '@' (FMRI without scheme), parse via Fmri::parse
if first . contains ( '@' ) {
2025-12-22 20:10:17 +01:00
if let Ok ( fmri ) = libips ::fmri ::Fmri ::parse ( first ) {
return fmri . stem ( ) . to_string ( ) ;
}
2025-08-26 14:53:31 +02:00
}
// Otherwise assume it's a stem token
first . trim_end_matches ( ',' ) . to_string ( )
}
fn parse_leaf_node ( node : & str ) -> ( String , DepConstraint ) {
2025-12-22 20:10:17 +01:00
let core = node
. split ( " for which " )
. next ( )
. unwrap_or ( node )
. trim ( )
. trim_end_matches ( ',' )
. to_string ( ) ;
2025-08-26 14:53:31 +02:00
let mut release : Option < String > = None ;
let mut branch : Option < String > = None ;
// Find release=
if let Some ( p ) = core . find ( " release= " ) {
let rest = & core [ p + " release= " . len ( ) .. ] ;
2025-12-22 20:10:17 +01:00
let end = rest
. find ( | c : char | c = = ' ' | | c = = ',' )
. unwrap_or ( rest . len ( ) ) ;
2025-08-26 14:53:31 +02:00
release = Some ( rest [ .. end ] . to_string ( ) ) ;
}
// Find branch=
if let Some ( p ) = core . find ( " branch= " ) {
let rest = & core [ p + " branch= " . len ( ) .. ] ;
2025-12-22 20:10:17 +01:00
let end = rest
. find ( | c : char | c = = ' ' | | c = = ',' )
. unwrap_or ( rest . len ( ) ) ;
2025-08-26 14:53:31 +02:00
branch = Some ( rest [ .. end ] . to_string ( ) ) ;
}
// Stem is first token
let stem = stem_from_node ( & core ) ;
( stem , DepConstraint { release , branch } )
2025-08-26 12:38:36 +02:00
}