Skip to main content

zoi_install/
pubgrub.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::fmt::Display;
4
5use colored::Colorize;
6use pubgrub::{Dependencies, DependencyProvider, Ranges};
7use rusqlite::params;
8use rustc_hash::FxHashMap;
9use semver::Version;
10use thiserror::Error;
11use zoi_core::types;
12use zoi_db as db;
13use zoi_resolver::resolve;
14
15/// Parses a package key from the `pkgs_v2` section of a `zoi.lua` file.
16///
17/// Keys are typically in the format `@repo/name` or `@repo/name:sub_package`.
18fn parse_pkgs_v2_key(key: &str) -> (String, String) {
19    let key = key.trim_start_matches('#');
20    let key = key.trim_start_matches('@');
21    if let Some((repo, name)) = key.split_once('/') {
22        let name = name.split(':').next().unwrap_or(name);
23        (repo.to_string(), name.to_string())
24    } else {
25        (String::new(), key.to_string())
26    }
27}
28
29/// Uniquely identifies a package within the `PubGrub` solver.
30#[derive(Clone, Debug, Eq, PartialEq, Hash)]
31pub struct PkgName {
32    /// The name of the package.
33    pub name: String,
34    /// An optional sub-package name.
35    pub sub_package: Option<String>,
36    /// The repository name where the package is hosted.
37    pub repo: String,
38    /// The registry handle (e.g. "zoidberg").
39    pub registry: String,
40    /// An explicit source URL, file path, or git reference, if provided.
41    pub explicit_source: Option<String>
42}
43
44impl Display for PkgName {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        if let Some(source) = &self.explicit_source {
47            return write!(f, "{source}");
48        }
49        if let Some(sub) = &self.sub_package {
50            write!(f, "#{}@{}/{}:{}", self.registry, self.repo, self.name, sub)
51        } else {
52            write!(f, "#{}@{}/{}", self.registry, self.repo, self.name)
53        }
54    }
55}
56
57/// A semantic version with an optional epoch for ordering.
58#[derive(Clone, Debug, Eq, PartialEq, Hash)]
59pub struct SemVersion {
60    /// The version epoch. Versions with higher epochs always sort higher.
61    pub epoch: u32,
62    /// The underlying semantic version.
63    pub v: Version,
64    /// The original raw version string before parsing and sanitization.
65    pub original: String
66}
67
68impl SemVersion {
69    /// Creates a new `SemVersion`.
70    pub fn new(epoch: u32, v: Version, original: String) -> Self {
71        Self { epoch, v, original }
72    }
73
74    /// Parses a version string into a `SemVersion`.
75    ///
76    /// Handles epoch prefixes (e.g. "1:1.2.3") and sanitizes non-standard
77    /// strings.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the version string is not a valid semver.
82    pub fn parse(v: &str) -> Result<Self, anyhow::Error> {
83        let (epoch, version_str) =
84            if let Some((e_str, v_str)) = v.split_once(':') {
85                if let Ok(e) = e_str.parse::<u32>() {
86                    (e, v_str)
87                } else {
88                    (0, v)
89                }
90            } else {
91                (0, v)
92            };
93
94        let clean = sanitize_version_string(version_str);
95        match Version::parse(&clean) {
96            Ok(parsed) => Ok(SemVersion {
97                epoch,
98                v: parsed,
99                original: v.to_string()
100            }),
101            Err(_) => {
102                // Fallback for extremely weird versions: use 0.0.0+original
103                Ok(SemVersion {
104                    epoch,
105                    v: Version::new(0, 0, 0),
106                    original: v.to_string()
107                })
108            }
109        }
110    }
111}
112
113impl Ord for SemVersion {
114    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115        self.epoch
116            .cmp(&other.epoch)
117            .then_with(|| self.v.cmp(&other.v))
118    }
119}
120
121impl PartialOrd for SemVersion {
122    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
123        Some(self.cmp(other))
124    }
125}
126
127impl Display for SemVersion {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(f, "{}", self.original)
130    }
131}
132
133/// Sanitizes non-standard version strings into valid `SemVer` format.
134///
135/// Logic:
136/// - Padds missing parts (e.g. "2.41" -> "2.41.0").
137/// - Converts extra parts into build metadata (e.g. "7.1.4.arch1" ->
138///   "7.1.4+arch1").
139/// - Cleans up prefixes and suffixes.
140fn sanitize_version_string(v: &str) -> String {
141    let v = v.trim_start_matches('v').replace('-', "+");
142    let parts: Vec<&str> = v.split('.').collect();
143
144    match parts.len() {
145        1 => {
146            if let Some(p0) = parts.first() {
147                format!("{p0}.0.0")
148            } else {
149                "0.0.0".to_string()
150            }
151        }
152        2 => {
153            if let (Some(p0), Some(p1)) = (parts.first(), parts.get(1)) {
154                format!("{p0}.{p1}.0")
155            } else {
156                "0.0.0".to_string()
157            }
158        }
159        len if len > 3 => {
160            if let (Some(p0), Some(p1), Some(p2)) =
161                (parts.first(), parts.get(1), parts.get(2))
162            {
163                let rest = parts.get(3..).unwrap_or(&[]).join(".");
164                format!("{p0}.{p1}.{p2}+{rest}")
165            } else {
166                v.clone()
167            }
168        }
169        _ => v.clone()
170    }
171}
172
173/// Errors that can occur during dependency resolution using the `PubGrub`
174/// solver.
175#[derive(Error, Debug)]
176pub enum ZoiSolverError {
177    /// A general dependency error with a descriptive message.
178    #[error("Dependency error: {0}")]
179    Dependency(String),
180    /// A version-related error.
181    #[error("Version error: {0}")]
182    Version(String),
183    /// An unexpected error from another part of the system.
184    #[error("Anyhow error: {0}")]
185    Anyhow(#[from] anyhow::Error),
186    /// Other miscellaneous errors.
187    #[error("Other error: {0}")]
188    Other(String)
189}
190
191/// Adapts Zoi's package and registry model to the `PubGrub` SAT solver.
192///
193/// This is the most performance-critical part of the resolution logic.
194/// It implements the `DependencyProvider` trait, which allows the solver
195/// to "ask" Zoi:
196/// - "What versions are available for this package?"
197/// - "What are the dependencies for this specific version?"
198///
199/// The adapter handles querying the local `SQLite` index, remote registries,
200/// and project-local `zoi.lua` overrides in a unified way.
201pub struct ZoiDependencyProvider {
202    /// The initial set of direct dependencies requested by the user.
203    pub root_deps: FxHashMap<PkgName, Ranges<SemVersion>>,
204    /// The raw string sources (e.g. "#reg@repo/pkg@ver") provided to the CLI.
205    pub initial_sources: Vec<String>,
206    /// The target installation scope (User, System, Project).
207    pub scope: Option<types::Scope>,
208    /// Whether to suppress non-critical warnings during resolution.
209    pub quiet: bool,
210    /// Automatically accept default choices for interactive dependency
211    /// options.
212    pub yes: bool,
213    /// Automatically include all optional dependencies.
214    pub all_optional: bool,
215    /// An in-memory cache of the registry used in 'mini' mode to avoid disk
216    /// I/O.
217    pub mini_index: Option<zoi_resolver::mini_resolve::MiniRegistryIndex>,
218    /// The loaded `zoi.lua` or `zoi.yaml` project configuration, if
219    /// applicable.
220    pub project_config: Option<zoi_project::config::ProjectConfig>,
221    /// Hard version constraints enforced by a project's lockfile or
222    /// configuration.
223    pub pkgs_v2_constraints: HashMap<(String, String), String>,
224    /// Build type used for selecting typed build dependencies.
225    pub build_type: Option<String>,
226    /// Memoization cache mapping a package+version to its resolved dependency
227    /// requirements. The `RefCell` allows interior mutability since the
228    /// `PubGrub` solver requires `&self`.
229    pub deps_cache: RefCell<
230        FxHashMap<
231            (PkgName, SemVersion),
232            FxHashMap<PkgName, Ranges<SemVersion>>
233        >
234    >,
235    /// Memoization cache storing the explicit options and optional
236    /// dependencies chosen by the user.
237    pub chosen_cache: RefCell<
238        FxHashMap<
239            (PkgName, SemVersion),
240            (Vec<String>, Vec<String>, Vec<String>)
241        >
242    >
243}
244
245/// Converts a `SemVer` requirement string into a `PubGrub` version range.
246///
247/// This function bridges the gap between the `semver` crate's flexible
248/// requirement strings and `pubgrub`'s mathematical version ranges.
249///
250/// Mapping Examples:
251/// - `1.2.3` -> `Ranges::singleton(1.2.3)` (Exact match)
252/// - `^1.2.3` -> `[1.2.3, 2.0.0)` (Caret: compatible updates)
253/// - `~1.2.3` -> `[1.2.3, 1.3.0)` (Tilde: patch-level updates)
254/// - `>=1.0.0, <2.0.0` -> `[1.0.0, 2.0.0)` (Intersection of ranges)
255///
256/// If a version string is not a valid `SemVer` requirement (e.g. a channel name
257/// like `@stable`), it is treated as a `Ranges::full()` to let Zoi's
258/// higher-level resolver handle the channel-to-version mapping.
259pub fn semver_to_range(req_str: &str) -> Ranges<SemVersion> {
260    let req_str_clean = req_str.trim_start_matches('@').trim_start_matches('v');
261
262    if let Ok(version) = SemVersion::parse(req_str_clean) {
263        return Ranges::singleton(version);
264    }
265
266    if let Ok(req) = semver::VersionReq::parse(req_str_clean) {
267        let mut range = Ranges::full();
268        for comparator in &req.comparators {
269            let v_str = format!(
270                "{}.{}.{}",
271                comparator.major,
272                comparator.minor.unwrap_or(0),
273                comparator.patch.unwrap_or(0)
274            );
275            let v = SemVersion {
276                epoch: 0,
277                v: Version {
278                    major: comparator.major,
279                    minor: comparator.minor.unwrap_or(0),
280                    patch: comparator.patch.unwrap_or(0),
281                    pre: comparator.pre.clone(),
282                    build: semver::BuildMetadata::EMPTY
283                },
284                original: v_str
285            };
286
287            let comp_range = match comparator.op {
288                semver::Op::Exact => Ranges::singleton(v),
289                semver::Op::Greater => Ranges::strictly_higher_than(v),
290                semver::Op::GreaterEq => Ranges::higher_than(v),
291                semver::Op::Less => Ranges::strictly_lower_than(v),
292                semver::Op::LessEq => Ranges::lower_than(v),
293                semver::Op::Tilde => {
294                    let next_minor_v = Version {
295                        major: comparator.major,
296                        minor: comparator.minor.unwrap_or(0) + 1,
297                        patch: 0,
298                        pre: semver::Prerelease::EMPTY,
299                        build: semver::BuildMetadata::EMPTY
300                    };
301                    let next_minor = SemVersion {
302                        epoch: 0,
303                        v: next_minor_v.clone(),
304                        original: next_minor_v.to_string()
305                    };
306                    Ranges::higher_than(v)
307                        .intersection(&Ranges::strictly_lower_than(next_minor))
308                }
309                semver::Op::Caret => {
310                    let next_v = if comparator.major > 0 {
311                        Version {
312                            major: comparator.major + 1,
313                            minor: 0,
314                            patch: 0,
315                            pre: semver::Prerelease::EMPTY,
316                            build: semver::BuildMetadata::EMPTY
317                        }
318                    } else if let Some(minor) = comparator.minor {
319                        if minor > 0 {
320                            Version {
321                                major: 0,
322                                minor: minor + 1,
323                                patch: 0,
324                                pre: semver::Prerelease::EMPTY,
325                                build: semver::BuildMetadata::EMPTY
326                            }
327                        } else {
328                            Version {
329                                major: 0,
330                                minor: 0,
331                                patch: comparator.patch.unwrap_or(0) + 1,
332                                pre: semver::Prerelease::EMPTY,
333                                build: semver::BuildMetadata::EMPTY
334                            }
335                        }
336                    } else {
337                        Version {
338                            major: 1,
339                            minor: 0,
340                            patch: 0,
341                            pre: semver::Prerelease::EMPTY,
342                            build: semver::BuildMetadata::EMPTY
343                        }
344                    };
345                    let next = SemVersion {
346                        epoch: 0,
347                        v: next_v.clone(),
348                        original: next_v.to_string()
349                    };
350                    Ranges::higher_than(v)
351                        .intersection(&Ranges::strictly_lower_than(next))
352                }
353                _ => Ranges::full()
354            };
355            range = range.intersection(&comp_range);
356        }
357        return range;
358    }
359
360    Ranges::full()
361}
362
363impl ZoiDependencyProvider {
364    /// Creates a new `ZoiDependencyProvider`.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if the registry index cannot be fetched in mini mode.
369    pub fn new(
370        root_deps: FxHashMap<PkgName, Ranges<SemVersion>>,
371        initial_sources: Vec<String>,
372        scope: Option<types::Scope>,
373        quiet: bool,
374        yes: bool,
375        all_optional: bool,
376        project_config: Option<zoi_project::config::ProjectConfig>,
377        build_type: Option<String>
378    ) -> Result<Self, anyhow::Error> {
379        let mini_index = if zoi_core::utils::is_mini_mode() {
380            Some(zoi_resolver::mini_resolve::fetch_registry_index()?)
381        } else {
382            None
383        };
384
385        let pkgs_v2_constraints = project_config
386            .as_ref()
387            .map(|config| {
388                config
389                    .pkgs_v2
390                    .iter()
391                    .filter_map(|(key, spec)| {
392                        spec.version
393                            .as_ref()
394                            .map(|v| (parse_pkgs_v2_key(key), v.clone()))
395                    })
396                    .collect::<HashMap<_, _>>()
397            })
398            .unwrap_or_default();
399
400        Ok(Self {
401            root_deps,
402            initial_sources,
403            scope,
404            quiet,
405            yes,
406            all_optional,
407            mini_index,
408            project_config,
409            pkgs_v2_constraints,
410            build_type,
411            deps_cache: RefCell::new(FxHashMap::default()),
412            chosen_cache: RefCell::new(FxHashMap::default())
413        })
414    }
415
416    /// Internal helper to convert a requirement string to a `PubGrub` range.
417    fn semver_to_range(req_str: &str) -> Ranges<SemVersion> {
418        semver_to_range(req_str)
419    }
420
421    /// Determines if a raw source string matches a resolved `PkgName`.
422    fn source_matches_package(&self, package: &PkgName, source: &str) -> bool {
423        if let Some(explicit_source) = &package.explicit_source {
424            let explicit_base = explicit_source
425                .rsplit_once('@')
426                .map_or(explicit_source.as_str(), |(base, _)| base);
427            let source_base =
428                source.rsplit_once('@').map_or(source, |(base, _)| base);
429            return explicit_base == source_base;
430        }
431
432        let Ok(req) = resolve::parse_source_string(source) else {
433            return false;
434        };
435
436        if req.name != package.name || req.sub_package != package.sub_package {
437            return false;
438        }
439
440        let Ok(resolved_source) =
441            resolve::resolve_source(source, self.scope, true, true)
442        else {
443            return false;
444        };
445
446        resolved_source.repo_name.unwrap_or_default() == package.repo
447            && resolved_source
448                .registry_handle
449                .unwrap_or_else(|| "zoidberg".to_string())
450                == package.registry
451    }
452
453    /// Retrieves all available versions for a given package from registries and
454    /// sources.
455    ///
456    /// # Errors
457    ///
458    /// Returns a `ZoiSolverError` if database queries fail.
459    pub fn get_versions(
460        &self,
461        package: &PkgName
462    ) -> Result<Vec<SemVersion>, ZoiSolverError> {
463        let mut all_versions = Vec::new();
464
465        if let Some(index) = &self.mini_index
466            && let Some(pkg_info) = index.packages.get(&package.name)
467            && let Ok(v) =
468                SemVersion::parse(pkg_info.version.trim_start_matches('v'))
469        {
470            all_versions.push(v);
471        }
472
473        if let Ok(version_strings) = db::get_all_versions(
474            &package.registry,
475            &package.name,
476            &package.repo
477        ) {
478            for v_str in version_strings {
479                if let Ok(v) = SemVersion::parse(&v_str) {
480                    all_versions.push(v);
481                }
482            }
483        }
484
485        for source in &self.initial_sources {
486            if self.source_matches_package(package, source)
487                && let Ok(req) = resolve::parse_source_string(source)
488                && let Some(v_spec) = req.version_spec
489                && let Ok(v) = SemVersion::parse(&v_spec)
490            {
491                all_versions.push(v);
492            }
493        }
494
495        let source_str = package.explicit_source.clone().unwrap_or_else(|| {
496            if let Some(sub) = &package.sub_package {
497                format!(
498                    "#{}@{}/{}:{}",
499                    package.registry, package.repo, package.name, sub
500                )
501            } else {
502                format!(
503                    "#{}@{}/{}",
504                    package.registry, package.repo, package.name
505                )
506            }
507        });
508
509        if let Ok(resolved) =
510            resolve::resolve_source(&source_str, self.scope, true, true)
511        {
512            let path_str = resolved.path.to_string_lossy();
513            if let Ok(pkg) = zoi_lua::parser::parse_lua_package(
514                &path_str, None, self.scope, true
515            ) {
516                if let Some(v_str) = &pkg.version
517                    && let Ok(v) = SemVersion::parse(v_str)
518                {
519                    all_versions.push(v);
520                }
521                if let Some(versions_map) = &pkg.versions {
522                    for channel in versions_map.keys() {
523                        if let Ok(v_str) =
524                            resolve::resolve_channel(versions_map, channel)
525                            && let Ok(v) = SemVersion::parse(&v_str)
526                        {
527                            all_versions.push(v);
528                        }
529                    }
530                }
531            }
532        }
533
534        if all_versions.is_empty()
535            && let Ok(conn) = db::open_connection(&package.registry)
536        {
537            let mut stmt = conn
538                .prepare("SELECT version FROM packages WHERE name = ?1")
539                .map_err(|e| ZoiSolverError::Other(e.to_string()))?;
540            let rows = stmt
541                .query_map(params![package.name], |row| {
542                    row.get::<_, Option<String>>(0)
543                })
544                .map_err(|e| ZoiSolverError::Other(e.to_string()))?;
545
546            for v_res in rows {
547                if let Ok(Some(v_str)) = v_res
548                    && let Ok(v) = SemVersion::parse(&v_str)
549                {
550                    all_versions.push(v);
551                }
552            }
553        }
554
555        all_versions.sort();
556        all_versions.dedup();
557
558        if let Some(version_spec) = self
559            .pkgs_v2_constraints
560            .get(&(package.repo.clone(), package.name.clone()))
561        {
562            let range = semver_to_range(version_spec);
563            all_versions.retain(|v| range.contains(v));
564        }
565
566        Ok(all_versions)
567    }
568}
569
570impl DependencyProvider for ZoiDependencyProvider {
571    type P = PkgName;
572    type V = SemVersion;
573    type VS = Ranges<SemVersion>;
574    type M = String;
575    type Priority = i32;
576    type Err = ZoiSolverError;
577
578    fn get_dependencies(
579        &self,
580        package: &Self::P,
581        version: &Self::V
582    ) -> Result<Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
583        if package.name == "$root" {
584            return Ok(Dependencies::Available(
585                pubgrub::DependencyConstraints::from_iter(
586                    self.root_deps.clone()
587                )
588            ));
589        }
590
591        let cache_key = (package.clone(), version.clone());
592        if let Some(cached) = self.deps_cache.borrow().get(&cache_key) {
593            return Ok(Dependencies::Available(
594                pubgrub::DependencyConstraints::from_iter(cached.clone())
595            ));
596        }
597
598        let version_str = version.to_string();
599
600        let mut chosen_opts = Vec::new();
601        let mut chosen_opts_opt = Vec::new();
602
603        if let Some(config) = &self.project_config {
604            let packages_key = if let Some(sub) = &package.sub_package {
605                format!("@{}/{}:{}", package.repo, package.name, sub)
606            } else {
607                format!("@{}/{}", package.repo, package.name)
608            };
609
610            if let Some(spec) = config.pkgs_v2.get(&packages_key) {
611                if let Some(opts) = &spec.options {
612                    chosen_opts.extend(opts.clone());
613                }
614                if let Some(opt_optionals) = &spec.optionals {
615                    chosen_opts_opt.extend(opt_optionals.clone());
616                }
617            }
618        }
619
620        let mut package_deps: Option<types::DependenciesV2> = None;
621
622        if let Some(config) = &self.project_config {
623            let packages_key = if let Some(sub) = &package.sub_package {
624                format!("@{}/{}:{}", package.repo, package.name, sub)
625            } else {
626                format!("@{}/{}", package.repo, package.name)
627            };
628
629            if let Some(spec) = config.pkgs_v2.get(&packages_key)
630                && spec.version.as_ref().is_none_or(|v| {
631                    let range = semver_to_range(v);
632                    if let Ok(pv) = SemVersion::parse(&version_str) {
633                        range.contains(&pv) || v == &version_str
634                    } else {
635                        v == &version_str
636                    }
637                })
638                && let Some(v1_deps) = &spec.dependencies
639            {
640                package_deps = Some(v1_deps.resolve(
641                    &chosen_opts,
642                    &chosen_opts_opt,
643                    package.sub_package.as_deref(),
644                    self.all_optional,
645                    self.build_type.as_deref()
646                ));
647            }
648        }
649
650        if package_deps.is_none() {
651            let dependencies_opt = db::get_package_dependencies(
652                &package.registry,
653                &package.name,
654                &version_str,
655                package.sub_package.as_deref(),
656                &package.repo
657            )
658            .ok()
659            .flatten();
660
661            let v1_deps = if let Some(deps_json) = dependencies_opt
662                && !deps_json.is_empty()
663            {
664                serde_json::from_str::<types::Dependencies>(&deps_json).ok()
665            } else {
666                let source =
667                    package.explicit_source.clone().unwrap_or_else(|| {
668                        if let Some(sub) = &package.sub_package {
669                            format!(
670                                "#{}@{}/{}:{}@{}",
671                                package.registry,
672                                package.repo,
673                                package.name,
674                                sub,
675                                version_str
676                            )
677                        } else {
678                            format!(
679                                "#{}@{}/{}@{}",
680                                package.registry,
681                                package.repo,
682                                package.name,
683                                version_str
684                            )
685                        }
686                    });
687
688                let pkg_res = resolve::resolve_package_and_version(
689                    &source, self.scope, self.quiet, self.yes
690                );
691
692                match pkg_res {
693                    Ok((pkg, _, _, _, _, _, _)) => pkg.dependencies,
694                    Err(e) => {
695                        println!(
696                            "{} Failed to resolve source for deps: {}",
697                            "::".bold().red(),
698                            e
699                        );
700                        None
701                    }
702                }
703            };
704
705            package_deps = v1_deps.map(|d| {
706                d.resolve(
707                    &chosen_opts,
708                    &chosen_opts_opt,
709                    package.sub_package.as_deref(),
710                    self.all_optional,
711                    self.build_type.as_deref()
712                )
713            });
714        }
715
716        let mut deps = FxHashMap::default();
717        let mut all_req = Vec::new();
718
719        if let Some(dependencies) = package_deps {
720            let mut groups = Vec::new();
721            groups.push(&dependencies.runtime);
722            for b in &dependencies.build {
723                groups.push(&b.packages);
724            }
725
726            for group_pkgs in groups {
727                for dep_str in group_pkgs {
728                    let dep_req = zoi_deps::parse_dependency_string(dep_str)
729                        .map_err(|e| {
730                            ZoiSolverError::Dependency(format!(
731                                "parse fail for '{dep_str}': {e}"
732                            ))
733                        })?;
734
735                    if dep_req.manager == "zoi" {
736                        let req =
737                            match resolve::parse_source_string(dep_req.package)
738                            {
739                                Ok(r) => r,
740                                Err(e) => {
741                                    println!(
742                                        "{} Dependency parse failed for '{}': \
743                                         {}",
744                                        "::".bold().red(),
745                                        dep_req.package,
746                                        e
747                                    );
748                                    return Err(ZoiSolverError::Dependency(
749                                        format!(
750                                            "parse source fail for '{}': {}",
751                                            dep_req.package, e
752                                        )
753                                    ));
754                                }
755                            };
756
757                        let resolved_dep = match resolve::resolve_source(
758                            dep_req.package,
759                            self.scope,
760                            false,
761                            self.yes
762                        ) {
763                            Ok(r) => r,
764                            Err(e) => {
765                                println!(
766                                    "{} Dependency resolution failed for \
767                                     '{}': {}",
768                                    "::".bold().red(),
769                                    dep_req.package,
770                                    e
771                                );
772                                return Err(ZoiSolverError::Dependency(
773                                    format!(
774                                        "resolve fail for '{}': {}",
775                                        dep_req.package, e
776                                    )
777                                ));
778                            }
779                        };
780
781                        let dep_name = PkgName {
782                            name: req.name,
783                            sub_package: req.sub_package,
784                            repo: resolved_dep
785                                .repo_name
786                                .clone()
787                                .unwrap_or_default(),
788                            registry: resolved_dep
789                                .registry_handle
790                                .clone()
791                                .unwrap_or_else(|| "zoidberg".to_string()),
792                            explicit_source: matches!(
793                                resolved_dep.source_type,
794                                zoi_core::types::SourceType::LocalFile
795                                    | zoi_core::types::SourceType::Url
796                                    | zoi_core::types::SourceType::GitRepo(_)
797                            )
798                            .then(|| dep_req.package.to_string())
799                        };
800
801                        let range = if req.version_spec.is_some() {
802                            match resolve::resolve_requested_version_spec(
803                                dep_req.package,
804                                self.scope,
805                                false,
806                                true
807                            ) {
808                                Ok(Some(v)) => Self::semver_to_range(&v),
809                                Ok(None) => Ranges::full(),
810                                Err(e) => {
811                                    println!(
812                                        "{} Version resolution failed for \
813                                         '{}': {}",
814                                        "::".bold().red(),
815                                        dep_req.package,
816                                        e
817                                    );
818                                    return Err(ZoiSolverError::Dependency(
819                                        format!(
820                                            "version resolve fail for '{}': {}",
821                                            dep_req.package, e
822                                        )
823                                    ));
824                                }
825                            }
826                        } else {
827                            Ranges::full()
828                        };
829
830                        deps.insert(dep_name, range);
831                    }
832                }
833            }
834            all_req.clone_from(&dependencies.runtime);
835        }
836
837        self.deps_cache
838            .borrow_mut()
839            .insert(cache_key.clone(), deps.clone());
840        self.chosen_cache
841            .borrow_mut()
842            .insert(cache_key, (chosen_opts, chosen_opts_opt, all_req));
843        Ok(Dependencies::Available(
844            pubgrub::DependencyConstraints::from_iter(deps)
845        ))
846    }
847
848    fn choose_version(
849        &self,
850        package: &Self::P,
851        versions: &pubgrub::Ranges<Self::V>
852    ) -> Result<Option<Self::V>, Self::Err> {
853        if package.name == "$root" {
854            return Ok(Some(SemVersion {
855                epoch: 0,
856                v: Version::new(0, 0, 0),
857                original: "0.0.0".to_string()
858            }));
859        }
860        let all_versions = self.get_versions(package)?;
861        let best_version =
862            all_versions.into_iter().rfind(|v| versions.contains(v));
863        Ok(best_version)
864    }
865
866    fn prioritize(
867        &self,
868        _package: &Self::P,
869        _range: &Self::VS,
870        _stats: &pubgrub::PackageResolutionStatistics
871    ) -> Self::Priority {
872        0
873    }
874}