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
15fn 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#[derive(Clone, Debug, Eq, PartialEq, Hash)]
31pub struct PkgName {
32 pub name: String,
34 pub sub_package: Option<String>,
36 pub repo: String,
38 pub registry: String,
40 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#[derive(Clone, Debug, Eq, PartialEq, Hash)]
59pub struct SemVersion {
60 pub epoch: u32,
62 pub v: Version,
64 pub original: String
66}
67
68impl SemVersion {
69 pub fn new(epoch: u32, v: Version, original: String) -> Self {
71 Self { epoch, v, original }
72 }
73
74 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 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
133fn 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#[derive(Error, Debug)]
176pub enum ZoiSolverError {
177 #[error("Dependency error: {0}")]
179 Dependency(String),
180 #[error("Version error: {0}")]
182 Version(String),
183 #[error("Anyhow error: {0}")]
185 Anyhow(#[from] anyhow::Error),
186 #[error("Other error: {0}")]
188 Other(String)
189}
190
191pub struct ZoiDependencyProvider {
202 pub root_deps: FxHashMap<PkgName, Ranges<SemVersion>>,
204 pub initial_sources: Vec<String>,
206 pub scope: Option<types::Scope>,
208 pub quiet: bool,
210 pub yes: bool,
213 pub all_optional: bool,
215 pub mini_index: Option<zoi_resolver::mini_resolve::MiniRegistryIndex>,
218 pub project_config: Option<zoi_project::config::ProjectConfig>,
221 pub pkgs_v2_constraints: HashMap<(String, String), String>,
224 pub build_type: Option<String>,
226 pub deps_cache: RefCell<
230 FxHashMap<
231 (PkgName, SemVersion),
232 FxHashMap<PkgName, Ranges<SemVersion>>
233 >
234 >,
235 pub chosen_cache: RefCell<
238 FxHashMap<
239 (PkgName, SemVersion),
240 (Vec<String>, Vec<String>, Vec<String>)
241 >
242 >
243}
244
245pub 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 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 fn semver_to_range(req_str: &str) -> Ranges<SemVersion> {
418 semver_to_range(req_str)
419 }
420
421 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 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}