1use colored::Colorize;
2use pubgrub::{Dependencies, DependencyProvider, Ranges};
3use rusqlite::params;
4use rustc_hash::FxHashMap;
5use semver::Version;
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::fmt::Display;
9use thiserror::Error;
10use zoi_core::types;
11use zoi_db as db;
12use zoi_resolver::resolve;
13
14fn parse_pkgs_v2_key(key: &str) -> (String, String) {
15 let key = key.trim_start_matches('#');
16 let key = key.trim_start_matches('@');
17 if let Some((repo, name)) = key.split_once('/') {
18 let name = name.split(':').next().unwrap_or(name);
19 (repo.to_string(), name.to_string())
20 } else {
21 (String::new(), key.to_string())
22 }
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Hash)]
26pub struct PkgName {
27 pub name: String,
28 pub sub_package: Option<String>,
29 pub repo: String,
30 pub registry: String,
31 pub explicit_source: Option<String>,
32}
33
34impl Display for PkgName {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 if let Some(source) = &self.explicit_source {
37 return write!(f, "{}", source);
38 }
39 if let Some(sub) = &self.sub_package {
40 write!(f, "#{}@{}/{}:{}", self.registry, self.repo, self.name, sub)
41 } else {
42 write!(f, "#{}@{}/{}", self.registry, self.repo, self.name)
43 }
44 }
45}
46
47#[derive(Clone, Debug, Eq, PartialEq, Hash)]
48pub struct SemVersion {
49 pub epoch: u32,
50 pub v: Version,
51 pub original: String,
52}
53
54impl SemVersion {
55 pub fn new(epoch: u32, v: Version, original: String) -> Self {
56 Self { epoch, v, original }
57 }
58
59 pub fn parse(v: &str) -> Result<Self, anyhow::Error> {
60 let (epoch, version_str) = if let Some((e_str, v_str)) = v.split_once(':') {
61 if let Ok(e) = e_str.parse::<u32>() {
62 (e, v_str)
63 } else {
64 (0, v)
65 }
66 } else {
67 (0, v)
68 };
69
70 let clean = sanitize_version_string(version_str);
71 match Version::parse(&clean) {
72 Ok(parsed) => Ok(SemVersion {
73 epoch,
74 v: parsed,
75 original: v.to_string(),
76 }),
77 Err(_) => {
78 Ok(SemVersion {
80 epoch,
81 v: Version::new(0, 0, 0),
82 original: v.to_string(),
83 })
84 }
85 }
86 }
87}
88
89impl Ord for SemVersion {
90 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
91 self.epoch
92 .cmp(&other.epoch)
93 .then_with(|| self.v.cmp(&other.v))
94 }
95}
96
97impl PartialOrd for SemVersion {
98 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
99 Some(self.cmp(other))
100 }
101}
102
103impl Display for SemVersion {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "{}", self.original)
106 }
107}
108
109fn sanitize_version_string(v: &str) -> String {
116 let v = v.trim_start_matches('v').replace('-', "+");
117 let parts: Vec<&str> = v.split('.').collect();
118
119 if parts.len() == 1 {
120 format!("{}.0.0", parts[0])
121 } else if parts.len() == 2 {
122 format!("{}.{}.0", parts[0], parts[1])
123 } else if parts.len() > 3 {
124 format!(
125 "{}.{}.{}+{}",
126 parts[0],
127 parts[1],
128 parts[2],
129 parts[3..].join(".")
130 )
131 } else {
132 v.to_string()
133 }
134}
135
136#[derive(Error, Debug)]
137pub enum ZoiSolverError {
138 #[error("Dependency error: {0}")]
139 Dependency(String),
140 #[error("Version error: {0}")]
141 Version(String),
142 #[error("Anyhow error: {0}")]
143 Anyhow(#[from] anyhow::Error),
144 #[error("Other error: {0}")]
145 Other(String),
146}
147
148pub struct ZoiDependencyProvider {
159 pub root_deps: FxHashMap<PkgName, Ranges<SemVersion>>,
161 pub initial_sources: Vec<String>,
163 pub scope: Option<types::Scope>,
165 pub quiet: bool,
167 pub yes: bool,
169 pub all_optional: bool,
171 pub mini_index: Option<zoi_resolver::mini_resolve::MiniRegistryIndex>,
173 pub project_config: Option<zoi_project::config::ProjectConfig>,
175 pub pkgs_v2_constraints: HashMap<(String, String), String>,
177 pub build_type: Option<String>,
179 pub deps_cache:
182 RefCell<FxHashMap<(PkgName, SemVersion), FxHashMap<PkgName, Ranges<SemVersion>>>>,
183 pub chosen_cache:
185 RefCell<FxHashMap<(PkgName, SemVersion), (Vec<String>, Vec<String>, Vec<String>)>>,
186}
187
188pub fn semver_to_range(req_str: &str) -> Ranges<SemVersion> {
203 let req_str_clean = req_str.trim_start_matches('@').trim_start_matches('v');
204
205 if let Ok(version) = SemVersion::parse(req_str_clean) {
206 return Ranges::singleton(version);
207 }
208
209 if let Ok(req) = semver::VersionReq::parse(req_str_clean) {
210 let mut range = Ranges::full();
211 for comparator in &req.comparators {
212 let v_str = format!(
213 "{}.{}.{}",
214 comparator.major,
215 comparator.minor.unwrap_or(0),
216 comparator.patch.unwrap_or(0)
217 );
218 let v = SemVersion {
219 epoch: 0,
220 v: Version {
221 major: comparator.major,
222 minor: comparator.minor.unwrap_or(0),
223 patch: comparator.patch.unwrap_or(0),
224 pre: comparator.pre.clone(),
225 build: semver::BuildMetadata::EMPTY,
226 },
227 original: v_str,
228 };
229
230 let comp_range = match comparator.op {
231 semver::Op::Exact => Ranges::singleton(v),
232 semver::Op::Greater => Ranges::strictly_higher_than(v),
233 semver::Op::GreaterEq => Ranges::higher_than(v),
234 semver::Op::Less => Ranges::strictly_lower_than(v),
235 semver::Op::LessEq => Ranges::lower_than(v),
236 semver::Op::Tilde => {
237 let next_minor_v = Version {
238 major: comparator.major,
239 minor: comparator.minor.unwrap_or(0) + 1,
240 patch: 0,
241 pre: semver::Prerelease::EMPTY,
242 build: semver::BuildMetadata::EMPTY,
243 };
244 let next_minor = SemVersion {
245 epoch: 0,
246 v: next_minor_v.clone(),
247 original: next_minor_v.to_string(),
248 };
249 Ranges::higher_than(v).intersection(&Ranges::strictly_lower_than(next_minor))
250 }
251 semver::Op::Caret => {
252 let next_v = if comparator.major > 0 {
253 Version {
254 major: comparator.major + 1,
255 minor: 0,
256 patch: 0,
257 pre: semver::Prerelease::EMPTY,
258 build: semver::BuildMetadata::EMPTY,
259 }
260 } else if let Some(minor) = comparator.minor {
261 if minor > 0 {
262 Version {
263 major: 0,
264 minor: minor + 1,
265 patch: 0,
266 pre: semver::Prerelease::EMPTY,
267 build: semver::BuildMetadata::EMPTY,
268 }
269 } else {
270 Version {
271 major: 0,
272 minor: 0,
273 patch: comparator.patch.unwrap_or(0) + 1,
274 pre: semver::Prerelease::EMPTY,
275 build: semver::BuildMetadata::EMPTY,
276 }
277 }
278 } else {
279 Version {
280 major: 1,
281 minor: 0,
282 patch: 0,
283 pre: semver::Prerelease::EMPTY,
284 build: semver::BuildMetadata::EMPTY,
285 }
286 };
287 let next = SemVersion {
288 epoch: 0,
289 v: next_v.clone(),
290 original: next_v.to_string(),
291 };
292 Ranges::higher_than(v).intersection(&Ranges::strictly_lower_than(next))
293 }
294 _ => Ranges::full(),
295 };
296 range = range.intersection(&comp_range);
297 }
298 return range;
299 }
300
301 Ranges::full()
302}
303
304impl ZoiDependencyProvider {
305 pub fn new(
306 root_deps: FxHashMap<PkgName, Ranges<SemVersion>>,
307 initial_sources: Vec<String>,
308 scope: Option<types::Scope>,
309 quiet: bool,
310 yes: bool,
311 all_optional: bool,
312 project_config: Option<zoi_project::config::ProjectConfig>,
313 build_type: Option<String>,
314 ) -> Result<Self, anyhow::Error> {
315 let mini_index = if zoi_core::utils::is_mini_mode() {
316 Some(zoi_resolver::mini_resolve::fetch_registry_index()?)
317 } else {
318 None
319 };
320
321 let pkgs_v2_constraints = project_config
322 .as_ref()
323 .map(|config| {
324 config
325 .pkgs_v2
326 .iter()
327 .filter_map(|(key, spec)| {
328 spec.version
329 .as_ref()
330 .map(|v| (parse_pkgs_v2_key(key), v.clone()))
331 })
332 .collect::<HashMap<_, _>>()
333 })
334 .unwrap_or_default();
335
336 Ok(Self {
337 root_deps,
338 initial_sources,
339 scope,
340 quiet,
341 yes,
342 all_optional,
343 mini_index,
344 project_config,
345 pkgs_v2_constraints,
346 build_type,
347 deps_cache: RefCell::new(FxHashMap::default()),
348 chosen_cache: RefCell::new(FxHashMap::default()),
349 })
350 }
351
352 fn semver_to_range(&self, req_str: &str) -> Ranges<SemVersion> {
353 semver_to_range(req_str)
354 }
355
356 fn source_matches_package(&self, package: &PkgName, source: &str) -> bool {
357 if let Some(explicit_source) = &package.explicit_source {
358 let explicit_base = explicit_source
359 .rsplit_once('@')
360 .map(|(base, _)| base)
361 .unwrap_or(explicit_source.as_str());
362 let source_base = source
363 .rsplit_once('@')
364 .map(|(base, _)| base)
365 .unwrap_or(source);
366 return explicit_base == source_base;
367 }
368
369 let Ok(req) = resolve::parse_source_string(source) else {
370 return false;
371 };
372
373 if req.name != package.name || req.sub_package != package.sub_package {
374 return false;
375 }
376
377 let Ok(resolved_source) = resolve::resolve_source(source, self.scope, true, true) else {
378 return false;
379 };
380
381 resolved_source.repo_name.unwrap_or_default() == package.repo
382 && resolved_source
383 .registry_handle
384 .unwrap_or_else(|| "zoidberg".to_string())
385 == package.registry
386 }
387
388 pub fn get_versions(&self, package: &PkgName) -> Result<Vec<SemVersion>, ZoiSolverError> {
389 let mut all_versions = Vec::new();
390
391 if let Some(index) = &self.mini_index
392 && let Some(pkg_info) = index.packages.get(&package.name)
393 && let Ok(v) = SemVersion::parse(pkg_info.version.trim_start_matches('v'))
394 {
395 all_versions.push(v);
396 }
397
398 if let Ok(version_strings) =
399 db::get_all_versions(&package.registry, &package.name, &package.repo)
400 {
401 for v_str in version_strings {
402 if let Ok(v) = SemVersion::parse(&v_str) {
403 all_versions.push(v);
404 }
405 }
406 }
407
408 for source in &self.initial_sources {
409 if self.source_matches_package(package, source)
410 && let Ok(req) = resolve::parse_source_string(source)
411 && let Some(v_spec) = req.version_spec
412 && let Ok(v) = SemVersion::parse(&v_spec)
413 {
414 all_versions.push(v);
415 }
416 }
417
418 let source_str = package.explicit_source.clone().unwrap_or_else(|| {
419 if let Some(sub) = &package.sub_package {
420 format!(
421 "#{}@{}/{}:{}",
422 package.registry, package.repo, package.name, sub
423 )
424 } else {
425 format!("#{}@{}/{}", package.registry, package.repo, package.name)
426 }
427 });
428
429 if let Ok(resolved) = resolve::resolve_source(&source_str, self.scope, true, true) {
430 let path_str = resolved.path.to_string_lossy();
431 if let Ok(pkg) = zoi_lua::parser::parse_lua_package(&path_str, None, self.scope, true) {
432 if let Some(v_str) = &pkg.version
433 && let Ok(v) = SemVersion::parse(v_str)
434 {
435 all_versions.push(v);
436 }
437 if let Some(versions_map) = &pkg.versions {
438 for channel in versions_map.keys() {
439 if let Ok(v_str) = resolve::resolve_channel(versions_map, channel)
440 && let Ok(v) = SemVersion::parse(&v_str)
441 {
442 all_versions.push(v);
443 }
444 }
445 }
446 }
447 }
448
449 if all_versions.is_empty()
450 && let Ok(conn) = db::open_connection(&package.registry)
451 {
452 let mut stmt = conn
453 .prepare("SELECT version FROM packages WHERE name = ?1")
454 .map_err(|e| ZoiSolverError::Other(e.to_string()))?;
455 let rows = stmt
456 .query_map(params![package.name], |row| row.get::<_, Option<String>>(0))
457 .map_err(|e| ZoiSolverError::Other(e.to_string()))?;
458
459 for v_res in rows {
460 if let Ok(Some(v_str)) = v_res
461 && let Ok(v) = SemVersion::parse(&v_str)
462 {
463 all_versions.push(v);
464 }
465 }
466 }
467
468 all_versions.sort();
469 all_versions.dedup();
470
471 if let Some(version_spec) = self
472 .pkgs_v2_constraints
473 .get(&(package.repo.clone(), package.name.clone()))
474 {
475 let range = semver_to_range(version_spec);
476 all_versions.retain(|v| range.contains(v));
477 }
478
479 Ok(all_versions)
480 }
481}
482
483impl DependencyProvider for ZoiDependencyProvider {
484 type P = PkgName;
485 type V = SemVersion;
486 type VS = Ranges<SemVersion>;
487 type M = String;
488 type Priority = i32;
489 type Err = ZoiSolverError;
490
491 fn get_dependencies(
492 &self,
493 package: &Self::P,
494 version: &Self::V,
495 ) -> Result<Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
496 if package.name == "$root" {
497 return Ok(Dependencies::Available(
498 pubgrub::DependencyConstraints::from_iter(self.root_deps.clone()),
499 ));
500 }
501
502 let cache_key = (package.clone(), version.clone());
503 if let Some(cached) = self.deps_cache.borrow().get(&cache_key) {
504 return Ok(Dependencies::Available(
505 pubgrub::DependencyConstraints::from_iter(cached.clone()),
506 ));
507 }
508
509 let version_str = version.to_string();
510
511 let mut chosen_opts = Vec::new();
512 let mut chosen_opts_opt = Vec::new();
513
514 if let Some(config) = &self.project_config {
515 let packages_key = if let Some(sub) = &package.sub_package {
516 format!("@{}/{}:{}", package.repo, package.name, sub)
517 } else {
518 format!("@{}/{}", package.repo, package.name)
519 };
520
521 if let Some(spec) = config.pkgs_v2.get(&packages_key) {
522 if let Some(opts) = &spec.options {
523 chosen_opts.extend(opts.clone());
524 }
525 if let Some(opt_optionals) = &spec.optionals {
526 chosen_opts_opt.extend(opt_optionals.clone());
527 }
528 }
529 }
530
531 let mut package_deps: Option<types::DependenciesV2> = None;
532
533 if let Some(config) = &self.project_config {
534 let packages_key = if let Some(sub) = &package.sub_package {
535 format!("@{}/{}:{}", package.repo, package.name, sub)
536 } else {
537 format!("@{}/{}", package.repo, package.name)
538 };
539
540 if let Some(spec) = config.pkgs_v2.get(&packages_key)
541 && spec.version.as_ref().is_none_or(|v| {
542 let range = semver_to_range(v);
543 if let Ok(pv) = SemVersion::parse(&version_str) {
544 range.contains(&pv) || v == &version_str
545 } else {
546 v == &version_str
547 }
548 })
549 && let Some(v1_deps) = &spec.dependencies
550 {
551 package_deps = Some(v1_deps.resolve(
552 &chosen_opts,
553 &chosen_opts_opt,
554 package.sub_package.as_deref(),
555 self.all_optional,
556 self.build_type.as_deref(),
557 ));
558 }
559 }
560
561 if package_deps.is_none() {
562 let dependencies_opt = db::get_package_dependencies(
563 &package.registry,
564 &package.name,
565 &version_str,
566 package.sub_package.as_deref(),
567 &package.repo,
568 )
569 .ok()
570 .flatten();
571
572 let v1_deps = if let Some(deps_json) = dependencies_opt
573 && !deps_json.is_empty()
574 {
575 serde_json::from_str::<types::Dependencies>(&deps_json).ok()
576 } else {
577 let source = package.explicit_source.clone().unwrap_or_else(|| {
578 if let Some(sub) = &package.sub_package {
579 format!(
580 "#{}@{}/{}:{}@{}",
581 package.registry, package.repo, package.name, sub, version_str
582 )
583 } else {
584 format!(
585 "#{}@{}/{}@{}",
586 package.registry, package.repo, package.name, version_str
587 )
588 }
589 });
590
591 let pkg_res =
592 resolve::resolve_package_and_version(&source, self.scope, self.quiet, self.yes);
593
594 match pkg_res {
595 Ok((pkg, _, _, _, _, _, _)) => pkg.dependencies,
596 Err(e) => {
597 println!(
598 "{} Failed to resolve source for deps: {}",
599 "::".bold().red(),
600 e
601 );
602 None
603 }
604 }
605 };
606
607 package_deps = v1_deps.map(|d| {
608 d.resolve(
609 &chosen_opts,
610 &chosen_opts_opt,
611 package.sub_package.as_deref(),
612 self.all_optional,
613 self.build_type.as_deref(),
614 )
615 });
616 }
617
618 let mut deps = FxHashMap::default();
619 let mut all_req = Vec::new();
620
621 if let Some(dependencies) = package_deps {
622 let mut groups = Vec::new();
623 groups.push(&dependencies.runtime);
624 for b in &dependencies.build {
625 groups.push(&b.packages);
626 }
627
628 for group_pkgs in groups {
629 for dep_str in group_pkgs {
630 let dep_req = zoi_deps::parse_dependency_string(dep_str).map_err(|e| {
631 ZoiSolverError::Dependency(format!("parse fail for '{}': {}", dep_str, e))
632 })?;
633
634 if dep_req.manager == "zoi" {
635 let req = match resolve::parse_source_string(dep_req.package) {
636 Ok(r) => r,
637 Err(e) => {
638 println!(
639 "{} Dependency parse failed for '{}': {}",
640 "::".bold().red(),
641 dep_req.package,
642 e
643 );
644 return Err(ZoiSolverError::Dependency(format!(
645 "parse source fail for '{}': {}",
646 dep_req.package, e
647 )));
648 }
649 };
650
651 let resolved_dep = match resolve::resolve_source(
652 dep_req.package,
653 self.scope,
654 false,
655 self.yes,
656 ) {
657 Ok(r) => r,
658 Err(e) => {
659 println!(
660 "{} Dependency resolution failed for '{}': {}",
661 "::".bold().red(),
662 dep_req.package,
663 e
664 );
665 return Err(ZoiSolverError::Dependency(format!(
666 "resolve fail for '{}': {}",
667 dep_req.package, e
668 )));
669 }
670 };
671
672 let dep_name = PkgName {
673 name: req.name,
674 sub_package: req.sub_package,
675 repo: resolved_dep.repo_name.clone().unwrap_or_default(),
676 registry: resolved_dep
677 .registry_handle
678 .clone()
679 .unwrap_or_else(|| "zoidberg".to_string()),
680 explicit_source: matches!(
681 resolved_dep.source_type,
682 zoi_core::types::SourceType::LocalFile
683 | zoi_core::types::SourceType::Url
684 | zoi_core::types::SourceType::GitRepo(_)
685 )
686 .then(|| dep_req.package.to_string()),
687 };
688
689 let range = if req.version_spec.is_some() {
690 match resolve::resolve_requested_version_spec(
691 dep_req.package,
692 self.scope,
693 false,
694 true,
695 ) {
696 Ok(Some(v)) => self.semver_to_range(&v),
697 Ok(None) => Ranges::full(),
698 Err(e) => {
699 println!(
700 "{} Version resolution failed for '{}': {}",
701 "::".bold().red(),
702 dep_req.package,
703 e
704 );
705 return Err(ZoiSolverError::Dependency(format!(
706 "version resolve fail for '{}': {}",
707 dep_req.package, e
708 )));
709 }
710 }
711 } else {
712 Ranges::full()
713 };
714
715 deps.insert(dep_name, range);
716 }
717 }
718 }
719 all_req = dependencies.runtime.clone();
720 }
721
722 self.deps_cache
723 .borrow_mut()
724 .insert(cache_key.clone(), deps.clone());
725 self.chosen_cache
726 .borrow_mut()
727 .insert(cache_key, (chosen_opts, chosen_opts_opt, all_req));
728 Ok(Dependencies::Available(
729 pubgrub::DependencyConstraints::from_iter(deps),
730 ))
731 }
732
733 fn choose_version(
734 &self,
735 package: &Self::P,
736 versions: &pubgrub::Ranges<Self::V>,
737 ) -> Result<Option<Self::V>, Self::Err> {
738 if package.name == "$root" {
739 return Ok(Some(SemVersion {
740 epoch: 0,
741 v: Version::new(0, 0, 0),
742 original: "0.0.0".to_string(),
743 }));
744 }
745 let all_versions = self.get_versions(package)?;
746 let best_version = all_versions.into_iter().rfind(|v| versions.contains(v));
747 Ok(best_version)
748 }
749
750 fn prioritize(
751 &self,
752 _package: &Self::P,
753 _range: &Self::VS,
754 _stats: &pubgrub::PackageResolutionStatistics,
755 ) -> Self::Priority {
756 0
757 }
758}