1use crate::tui::MultiProgress;
64use crate::{Config, RunContext, Sandbox};
65use anyhow::{Context, Result, bail};
66use indexmap::IndexMap;
67use petgraph::graphmap::DiGraphMap;
68use pkgsrc::{Depend, PkgName, PkgPath, ScanIndex};
69use rayon::prelude::*;
70use std::collections::{HashMap, HashSet};
71use std::io::BufReader;
72use std::sync::atomic::{AtomicBool, Ordering};
73use std::sync::{Arc, Mutex};
74use std::time::{Duration, Instant};
75use tracing::{debug, error, info, trace};
76
77#[derive(Clone, Debug)]
81pub enum SkipReason {
82 PkgSkipReason(String),
87 PkgFailReason(String),
92}
93
94#[derive(Clone, Debug)]
96pub struct SkippedPackage {
97 pub pkgname: PkgName,
99 pub pkgpath: Option<PkgPath>,
101 pub reason: SkipReason,
103 pub index: Option<ResolvedIndex>,
105}
106
107#[derive(Clone, Debug)]
109pub struct ScanFailure {
110 pub pkgpath: PkgPath,
112 pub error: String,
114}
115
116#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
120pub struct ResolvedIndex {
121 pub index: ScanIndex,
123 pub depends: Vec<PkgName>,
125}
126
127impl ResolvedIndex {
128 pub fn from_scan_index(index: ScanIndex) -> Self {
130 Self { index, depends: Vec::new() }
131 }
132}
133
134impl std::ops::Deref for ResolvedIndex {
135 type Target = ScanIndex;
136 fn deref(&self) -> &Self::Target {
137 &self.index
138 }
139}
140
141impl std::ops::DerefMut for ResolvedIndex {
142 fn deref_mut(&mut self) -> &mut Self::Target {
143 &mut self.index
144 }
145}
146
147impl std::fmt::Display for ResolvedIndex {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.index)?;
150 if !self.depends.is_empty() {
152 write!(f, "DEPENDS=")?;
153 for (i, d) in self.depends.iter().enumerate() {
154 if i > 0 {
155 write!(f, " ")?;
156 }
157 write!(f, "{d}")?;
158 }
159 writeln!(f)?;
160 }
161 Ok(())
162 }
163}
164
165#[derive(Clone, Debug, Default)]
170pub struct ScanResult {
171 pub buildable: IndexMap<PkgName, ResolvedIndex>,
176 pub skipped: Vec<SkippedPackage>,
178 pub scan_failed: Vec<ScanFailure>,
180 pub all_ordered: Vec<(ResolvedIndex, Option<SkipReason>)>,
183}
184
185#[derive(Debug, Default)]
218pub struct Scan {
219 config: Config,
220 sandbox: Sandbox,
221 incoming: HashSet<PkgPath>,
222 done: IndexMap<PkgPath, Vec<ScanIndex>>,
223 cache: IndexMap<PkgPath, Vec<ScanIndex>>,
225 resolved: IndexMap<PkgName, ResolvedIndex>,
226 full_tree: bool,
229 scan_failures: Vec<(PkgPath, String)>,
231}
232
233impl Scan {
234 pub fn new(config: &Config) -> Scan {
235 let sandbox = Sandbox::new(config);
236 debug!(pkgsrc = %config.pkgsrc().display(),
237 make = %config.make().display(),
238 scan_threads = config.scan_threads(),
239 "Created new Scan instance"
240 );
241 Scan {
242 config: config.clone(),
243 sandbox,
244 full_tree: true,
245 ..Default::default()
246 }
247 }
248
249 pub fn add(&mut self, pkgpath: &PkgPath) {
250 info!(pkgpath = %pkgpath.as_path().display(), "Adding package to scan queue");
251 self.full_tree = false;
252 self.incoming.insert(pkgpath.clone());
253 }
254
255 pub fn load_cached(
263 &mut self,
264 cached: IndexMap<PkgPath, Vec<ScanIndex>>,
265 ) -> usize {
266 info!(cached_count = cached.len(), "Loading cached scan results");
267
268 self.cache = cached.clone();
270
271 if self.full_tree {
272 self.done = cached;
274 } else {
275 let mut relevant: HashSet<PkgPath> = self.incoming.clone();
277 let mut to_process: Vec<PkgPath> =
278 self.incoming.iter().cloned().collect();
279
280 while let Some(pkgpath) = to_process.pop() {
282 if let Some(indexes) = cached.get(&pkgpath) {
283 for pkg in indexes {
284 if let Some(ref all_deps) = pkg.all_depends {
285 for dep in all_deps {
286 if relevant.insert(dep.pkgpath().clone()) {
287 to_process.push(dep.pkgpath().clone());
288 }
289 }
290 }
291 }
292 }
293 }
294
295 for (pkgpath, indexes) in cached {
297 if relevant.contains(&pkgpath) {
298 self.done.insert(pkgpath, indexes);
299 }
300 }
301 }
302
303 for indexes in self.done.values() {
305 for pkg in indexes {
306 if let Some(ref all_deps) = pkg.all_depends {
307 for dep in all_deps {
308 if !self.done.contains_key(dep.pkgpath()) {
309 self.incoming.insert(dep.pkgpath().clone());
310 }
311 }
312 }
313 }
314 }
315
316 self.incoming.retain(|p| !self.done.contains_key(p));
317 self.done.len()
318 }
319
320 pub fn completed(&self) -> &IndexMap<PkgPath, Vec<ScanIndex>> {
322 &self.done
323 }
324
325 fn load_cached_recursive(&mut self, pkgpath: PkgPath) -> usize {
328 let mut count = 0;
329 let mut to_load = vec![pkgpath];
330
331 while let Some(path) = to_load.pop() {
332 if self.done.contains_key(&path) {
333 continue;
334 }
335 if let Some(indexes) = self.cache.get(&path).cloned() {
336 for pkg in &indexes {
338 if let Some(ref all_deps) = pkg.all_depends {
339 for dep in all_deps {
340 if !self.done.contains_key(dep.pkgpath()) {
341 to_load.push(dep.pkgpath().clone());
342 }
343 }
344 }
345 }
346 self.done.insert(path, indexes);
347 count += 1;
348 }
349 }
350
351 count
352 }
353
354 fn discover_packages(&mut self) -> anyhow::Result<()> {
356 println!("Discovering packages...");
357 let pkgsrc = self.config.pkgsrc().display();
358 let make = self.config.make().display();
359
360 let script = format!(
362 "cd {} && {} show-subdir-var VARNAME=SUBDIR\n",
363 pkgsrc, make
364 );
365 let child = self.sandbox.execute_script(0, &script, vec![])?;
366 let output = child
367 .wait_with_output()
368 .context("Failed to run show-subdir-var")?;
369
370 if !output.status.success() {
371 let stderr = String::from_utf8_lossy(&output.stderr);
372 bail!("Failed to get categories: {}", stderr);
373 }
374
375 let stdout = String::from_utf8_lossy(&output.stdout);
376 let entries: Vec<&str> = stdout.split_whitespace().collect();
377
378 for entry in entries {
379 if entry.contains('/') {
380 if let Ok(pkgpath) = PkgPath::new(entry) {
382 self.incoming.insert(pkgpath);
383 }
384 } else {
385 let script = format!(
387 "cd {}/{} && {} show-subdir-var VARNAME=SUBDIR\n",
388 pkgsrc, entry, make
389 );
390 let child = self.sandbox.execute_script(0, &script, vec![])?;
391 let cat_output = child.wait_with_output();
392
393 match cat_output {
394 Ok(o) if o.status.success() => {
395 let pkgs = String::from_utf8_lossy(&o.stdout);
396 for pkg in pkgs.split_whitespace() {
397 let path = format!("{}/{}", entry, pkg);
398 if let Ok(pkgpath) = PkgPath::new(&path) {
399 self.incoming.insert(pkgpath);
400 }
401 }
402 }
403 Ok(o) => {
404 let stderr = String::from_utf8_lossy(&o.stderr);
405 debug!(category = entry, stderr = %stderr,
406 "Failed to get packages for category");
407 }
408 Err(e) => {
409 debug!(category = entry, error = %e,
410 "Failed to run make in category");
411 }
412 }
413 }
414 }
415
416 info!(discovered = self.incoming.len(), "Package discovery complete");
417 println!("Discovered {} package paths", self.incoming.len());
418
419 Ok(())
420 }
421
422 pub fn start(&mut self, ctx: &RunContext) -> anyhow::Result<bool> {
423 info!(
424 incoming_count = self.incoming.len(),
425 sandbox_enabled = self.sandbox.enabled(),
426 "Starting package scan"
427 );
428
429 let pool = rayon::ThreadPoolBuilder::new()
430 .num_threads(self.config.scan_threads())
431 .build()
432 .context("Failed to build scan thread pool")?;
433
434 let shutdown_flag = Arc::clone(&ctx.shutdown);
435 let stats = ctx.stats.clone();
436
437 let script_envs = self.config.script_env();
442
443 if self.sandbox.enabled() {
444 println!("Creating sandbox...");
445 if let Err(e) = self.sandbox.create(0) {
446 if let Err(destroy_err) = self.sandbox.destroy(0) {
447 eprintln!(
448 "Warning: failed to destroy sandbox: {}",
449 destroy_err
450 );
451 }
452 return Err(e);
453 }
454
455 if let Some(pre_build) = self.config.script("pre-build") {
457 debug!("Running pre-build script");
458 let child = self.sandbox.execute(
459 0,
460 pre_build,
461 script_envs.clone(),
462 None,
463 None,
464 )?;
465 let output = child
466 .wait_with_output()
467 .context("Failed to wait for pre-build")?;
468 if !output.status.success() {
469 let stderr = String::from_utf8_lossy(&output.stderr);
470 error!(exit_code = ?output.status.code(), stderr = %stderr, "pre-build script failed");
471 }
472 }
473 }
474
475 if self.full_tree && self.incoming.is_empty() {
477 self.discover_packages()?;
478 self.incoming.retain(|p| !self.done.contains_key(p));
479 }
480
481 if self.incoming.is_empty() {
483 if !self.done.is_empty() {
484 println!(
485 "All {} package paths already scanned",
486 self.done.len()
487 );
488 }
489 return Ok(false);
490 }
491
492 println!("Scanning packages...");
493
494 let cached_count = self.done.len();
497 let total_count = cached_count + self.incoming.len();
498 let progress = Arc::new(Mutex::new(
499 MultiProgress::new(
500 "Scanning",
501 "Scanned",
502 total_count,
503 self.config.scan_threads(),
504 )
505 .expect("Failed to initialize progress display"),
506 ));
507
508 if cached_count > 0 {
510 if let Ok(mut p) = progress.lock() {
511 p.state_mut().cached = cached_count;
512 }
513 }
514
515 let stop_refresh = Arc::new(AtomicBool::new(false));
517
518 let progress_refresh = Arc::clone(&progress);
520 let stop_flag = Arc::clone(&stop_refresh);
521 let shutdown_for_refresh = Arc::clone(&shutdown_flag);
522 let refresh_thread = std::thread::spawn(move || {
523 while !stop_flag.load(Ordering::Relaxed)
524 && !shutdown_for_refresh.load(Ordering::SeqCst)
525 {
526 if let Ok(mut p) = progress_refresh.lock() {
527 let _ = p.poll_events();
529 let _ = p.render();
530 }
531 std::thread::sleep(Duration::from_millis(50));
532 }
533 });
534
535 let mut interrupted = false;
541 loop {
542 if shutdown_flag.load(Ordering::Relaxed) {
544 stop_refresh.store(true, Ordering::Relaxed);
546 if let Ok(mut p) = progress.lock() {
547 let _ = p.finish_interrupted();
548 }
549 interrupted = true;
550 break;
551 }
552
553 let mut parpaths: Vec<(PkgPath, Result<Vec<ScanIndex>>)> =
557 self.incoming.iter().map(|p| (p.clone(), Ok(vec![]))).collect();
558
559 let progress_clone = Arc::clone(&progress);
560 let shutdown_clone = Arc::clone(&shutdown_flag);
561 let stats_clone = stats.clone();
562 pool.install(|| {
563 parpaths.par_iter_mut().for_each(|pkg| {
564 if shutdown_clone.load(Ordering::Relaxed) {
566 return;
567 }
568
569 let (pkgpath, result) = pkg;
570 let pathname =
571 pkgpath.as_path().to_string_lossy().to_string();
572
573 let thread_id = rayon::current_thread_index().unwrap_or(0);
575
576 if let Ok(mut p) = progress_clone.lock() {
578 p.state_mut().set_worker_active(thread_id, &pathname);
579 }
580
581 let scan_start = Instant::now();
582 *result = self.scan_pkgpath(pkgpath);
583 let scan_duration = scan_start.elapsed();
584
585 if let Some(ref s) = stats_clone {
587 s.scan(&pathname, scan_duration, result.is_ok());
588 }
589
590 if let Ok(mut p) = progress_clone.lock() {
592 p.state_mut().set_worker_idle(thread_id);
593 if result.is_ok() {
594 p.state_mut().increment_completed();
595 } else {
596 p.state_mut().increment_failed();
597 }
598 }
599 });
600 });
601
602 if shutdown_flag.load(Ordering::Relaxed) {
604 stop_refresh.store(true, Ordering::Relaxed);
606 if let Ok(mut p) = progress.lock() {
607 let _ = p.finish_interrupted();
608 }
609 interrupted = true;
610 break;
611 }
612
613 let mut new_incoming: HashSet<PkgPath> = HashSet::new();
619 let mut loaded_from_cache = 0usize;
620 for (pkgpath, scanpkgs) in parpaths.drain(..) {
621 let scanpkgs = match scanpkgs {
622 Ok(pkgs) => pkgs,
623 Err(e) => {
624 self.scan_failures
625 .push((pkgpath.clone(), e.to_string()));
626 self.done.insert(pkgpath.clone(), vec![]);
627 continue;
628 }
629 };
630 self.done.insert(pkgpath.clone(), scanpkgs.clone());
631 for pkg in scanpkgs {
633 if let Some(ref all_deps) = pkg.all_depends {
634 for dep in all_deps {
635 let dep_path = dep.pkgpath();
636 if self.done.contains_key(dep_path)
637 || self.incoming.contains(dep_path)
638 || new_incoming.contains(dep_path)
639 {
640 continue;
641 }
642 if self.cache.contains_key(dep_path) {
644 loaded_from_cache += self
645 .load_cached_recursive(dep_path.clone());
646 } else {
647 new_incoming.insert(dep_path.clone());
648 if let Ok(mut p) = progress.lock() {
649 p.state_mut().total += 1;
650 }
651 }
652 }
653 }
654 }
655 }
656 if loaded_from_cache > 0 {
657 if let Ok(mut p) = progress.lock() {
658 p.state_mut().total += loaded_from_cache;
659 p.state_mut().cached += loaded_from_cache;
660 }
661 }
662
663 self.incoming = new_incoming;
669 if self.incoming.is_empty() {
670 break;
671 }
672 }
673
674 stop_refresh.store(true, Ordering::Relaxed);
676 let _ = refresh_thread.join();
677
678 if !interrupted {
681 if let Ok(mut p) = progress.lock() {
682 let _ = p.finish();
683 }
684 }
685
686 if self.sandbox.enabled() {
687 if let Some(post_build) = self.config.script("post-build") {
689 debug!("Running post-build script");
690 let child = self.sandbox.execute(
691 0,
692 post_build,
693 script_envs,
694 None,
695 None,
696 )?;
697 let output = child
698 .wait_with_output()
699 .context("Failed to wait for post-build")?;
700 if !output.status.success() {
701 let stderr = String::from_utf8_lossy(&output.stderr);
702 error!(exit_code = ?output.status.code(), stderr = %stderr, "post-build script failed");
703 }
704 }
705
706 self.sandbox.destroy(0)?;
707 }
708
709 if interrupted {
710 return Ok(true);
711 }
712
713 Ok(false)
714 }
715
716 pub fn scan_errors(&self) -> impl Iterator<Item = &str> {
718 self.scan_failures.iter().map(|(_, e)| e.as_str())
719 }
720
721 pub fn scan_failures(&self) -> &[(PkgPath, String)] {
723 &self.scan_failures
724 }
725
726 pub fn scan_pkgpath(
731 &self,
732 pkgpath: &PkgPath,
733 ) -> anyhow::Result<Vec<ScanIndex>> {
734 let pkgpath_str = pkgpath.as_path().display().to_string();
735 debug!(pkgpath = %pkgpath_str, "Scanning package");
736
737 let bmake = self.config.make().display().to_string();
738 let pkgsrcdir = self.config.pkgsrc().display().to_string();
739 let script = format!(
740 "cd {}/{} && {} pbulk-index\n",
741 pkgsrcdir, pkgpath_str, bmake
742 );
743
744 let scan_env = self.config.scan_env();
745 trace!(pkgpath = %pkgpath_str,
746 script = %script,
747 scan_env = ?scan_env,
748 "Executing pkg-scan"
749 );
750 let child = self.sandbox.execute_script(0, &script, scan_env)?;
751 let output = child.wait_with_output()?;
752
753 if !output.status.success() {
754 let stderr = String::from_utf8_lossy(&output.stderr);
755 error!(pkgpath = %pkgpath_str,
756 exit_code = ?output.status.code(),
757 stderr = %stderr,
758 "pkg-scan script failed"
759 );
760 let stderr = stderr.trim();
761 let msg = if stderr.is_empty() {
762 format!("Scan failed for {}", pkgpath_str)
763 } else {
764 format!("Scan failed for {}: {}", pkgpath_str, stderr)
765 };
766 bail!(msg);
767 }
768
769 let stdout_str = String::from_utf8_lossy(&output.stdout);
770 trace!(pkgpath = %pkgpath_str,
771 stdout_len = stdout_str.len(),
772 stdout = %stdout_str,
773 "pkg-scan script output"
774 );
775
776 let reader = BufReader::new(&output.stdout[..]);
777 let mut index: Vec<ScanIndex> =
778 ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
779
780 info!(pkgpath = %pkgpath_str,
781 packages_found = index.len(),
782 "Scan complete for pkgpath"
783 );
784
785 for pkg in &mut index {
789 pkg.pkg_location = Some(pkgpath.clone());
790 debug!(pkgpath = %pkgpath_str,
791 pkgname = %pkg.pkgname.pkgname(),
792 skip_reason = ?pkg.pkg_skip_reason,
793 fail_reason = ?pkg.pkg_fail_reason,
794 depends_count = pkg.all_depends.as_ref().map_or(0, |v| v.len()),
795 "Found package in scan"
796 );
797 }
798
799 Ok(index)
800 }
801
802 pub fn scanned(&self) -> impl Iterator<Item = &ScanIndex> {
804 self.done.values().flatten()
805 }
806
807 pub fn resolve(&mut self) -> Result<ScanResult> {
819 info!(
820 done_pkgpaths = self.done.len(),
821 "Starting dependency resolution"
822 );
823
824 let mut pkgnames: indexmap::IndexSet<PkgName> =
833 indexmap::IndexSet::new();
834
835 let mut skip_reasons: HashMap<PkgName, SkipReason> = HashMap::new();
837
838 for (pkgpath, index) in &self.done {
840 debug!(pkgpath = %pkgpath.as_path().display(),
841 packages_in_index = index.len(),
842 "Processing done entry"
843 );
844 }
845
846 for index in self.done.values() {
847 for pkg in index {
848 if pkgnames.contains(&pkg.pkgname) {
851 debug!(pkgname = %pkg.pkgname.pkgname(),
852 multi_version = ?pkg.multi_version,
853 "Skipping duplicate PKGNAME"
854 );
855 continue;
856 }
857
858 if let Some(reason) = &pkg.pkg_skip_reason {
860 if !reason.is_empty() {
861 info!(pkgname = %pkg.pkgname.pkgname(),
862 reason = %reason,
863 "Package has PKG_SKIP_REASON"
864 );
865 skip_reasons.insert(
866 pkg.pkgname.clone(),
867 SkipReason::PkgSkipReason(reason.clone()),
868 );
869 }
870 }
871 if let Some(reason) = &pkg.pkg_fail_reason {
872 if !reason.is_empty()
873 && !skip_reasons.contains_key(&pkg.pkgname)
874 {
875 info!(pkgname = %pkg.pkgname.pkgname(),
876 reason = %reason,
877 "Package has PKG_FAIL_REASON"
878 );
879 skip_reasons.insert(
880 pkg.pkgname.clone(),
881 SkipReason::PkgFailReason(reason.clone()),
882 );
883 }
884 }
885
886 debug!(pkgname = %pkg.pkgname.pkgname(),
887 "Adding package to resolved set"
888 );
889 pkgnames.insert(pkg.pkgname.clone());
890 self.resolved.insert(
891 pkg.pkgname.clone(),
892 ResolvedIndex::from_scan_index(pkg.clone()),
893 );
894 }
895 }
896
897 info!(
898 resolved_count = self.resolved.len(),
899 skip_reasons_count = skip_reasons.len(),
900 "Initial resolution complete"
901 );
902
903 let mut match_cache: HashMap<Depend, PkgName> = HashMap::new();
908
909 let mut skip_due_to_dep: HashMap<PkgName, String> = HashMap::new();
914 let mut errors: Vec<(PkgName, String)> = Vec::new();
915
916 let is_satisfied = |depends: &[PkgName], pattern: &pkgsrc::Pattern| {
918 depends.iter().any(|existing| pattern.matches(existing.pkgname()))
919 };
920
921 for pkg in self.resolved.values_mut() {
922 let all_deps = match pkg.all_depends.clone() {
923 Some(deps) => deps,
924 None => continue,
925 };
926 for depend in &all_deps {
927 if let Some(pkgname) = match_cache.get(depend) {
930 if !is_satisfied(&pkg.depends, depend.pattern())
931 && !pkg.depends.contains(pkgname)
932 {
933 pkg.depends.push(pkgname.clone());
934 }
935 continue;
936 }
937 let mut candidates: Vec<&PkgName> = Vec::new();
942 for candidate in &pkgnames {
943 if depend.pattern().matches(candidate.pkgname()) {
944 candidates.push(candidate);
945 }
946 }
947
948 let mut best: Option<&PkgName> = None;
951 let mut match_error: Option<pkgsrc::PatternError> = None;
952 for candidate in candidates {
953 best = match best {
954 None => Some(candidate),
955 Some(current) => {
956 match depend.pattern().best_match_pbulk(
957 current.pkgname(),
958 candidate.pkgname(),
959 ) {
960 Ok(Some(m)) if m == candidate.pkgname() => {
961 Some(candidate)
962 }
963 Ok(_) => Some(current),
964 Err(e) => {
965 match_error = Some(e);
966 break;
967 }
968 }
969 }
970 };
971 }
972 if let Some(e) = match_error {
973 errors.push((
974 pkg.index.pkgname.clone(),
975 format!(
976 "Pattern error for {} in {}: {}",
977 depend.pattern().pattern(),
978 pkg.index.pkgname.pkgname(),
979 e
980 ),
981 ));
982 continue;
983 }
984 if let Some(pkgname) = best {
986 if !is_satisfied(&pkg.depends, depend.pattern())
987 && !pkg.depends.contains(pkgname)
988 {
989 pkg.depends.push(pkgname.clone());
990 }
991 match_cache.insert(depend.clone(), pkgname.clone());
992 } else {
993 errors.push((
995 pkg.index.pkgname.clone(),
996 format!(
997 "No match found for {} in {}",
998 depend.pattern().pattern(),
999 pkg.index.pkgname.pkgname()
1000 ),
1001 ));
1002 }
1003 }
1004 }
1005
1006 loop {
1011 let mut new_skips: HashMap<PkgName, String> = HashMap::new();
1012
1013 for pkg in self.resolved.values() {
1014 if skip_due_to_dep.contains_key(&pkg.pkgname)
1015 || skip_reasons.contains_key(&pkg.pkgname)
1016 {
1017 continue;
1018 }
1019 for dep in &pkg.depends {
1020 if skip_due_to_dep.contains_key(dep)
1021 || skip_reasons.contains_key(dep)
1022 {
1023 new_skips.insert(
1025 pkg.pkgname.clone(),
1026 format!("Dependency {} skipped", dep.pkgname()),
1027 );
1028 break;
1029 }
1030 }
1031 }
1032
1033 if new_skips.is_empty() {
1034 break;
1035 }
1036 skip_due_to_dep.extend(new_skips);
1037 }
1038
1039 for (pkgname, reason) in skip_due_to_dep.iter() {
1041 if !skip_reasons.contains_key(pkgname) {
1042 skip_reasons.insert(
1043 pkgname.clone(),
1044 SkipReason::PkgSkipReason(reason.clone()),
1045 );
1046 }
1047 }
1048
1049 let errors: Vec<String> = errors
1051 .into_iter()
1052 .filter(|(pkgname, _)| !skip_reasons.contains_key(pkgname))
1053 .map(|(_, message)| message)
1054 .collect();
1055
1056 let mut all_ordered: Vec<(ResolvedIndex, Option<SkipReason>)> =
1058 Vec::new();
1059 let mut buildable: IndexMap<PkgName, ResolvedIndex> = IndexMap::new();
1060 let mut skipped: Vec<SkippedPackage> = Vec::new();
1061
1062 for (pkgname, index) in std::mem::take(&mut self.resolved) {
1063 let reason = skip_reasons.remove(&pkgname);
1064 all_ordered.push((index.clone(), reason.clone()));
1065 if let Some(r) = reason {
1066 skipped.push(SkippedPackage {
1067 pkgname: index.pkgname.clone(),
1068 pkgpath: index.pkg_location.clone(),
1069 reason: r,
1070 index: Some(index),
1071 });
1072 } else {
1073 buildable.insert(pkgname, index);
1074 }
1075 }
1076
1077 debug!(
1081 buildable_count = buildable.len(),
1082 "Checking for circular dependencies"
1083 );
1084 let mut graph = DiGraphMap::new();
1085 for (pkgname, index) in &buildable {
1086 for dep in &index.depends {
1087 graph.add_edge(dep.pkgname(), pkgname.pkgname(), ());
1088 }
1089 }
1090 let cycle_error = find_cycle(&graph).map(|cycle| {
1091 let mut err = "Circular dependencies detected:\n".to_string();
1092 for n in cycle.iter().rev() {
1093 err.push_str(&format!("\t{}\n", n));
1094 }
1095 err.push_str(&format!("\t{}", cycle.last().unwrap()));
1096 error!(cycle = ?cycle, "Circular dependency detected");
1097 err
1098 });
1099
1100 info!(
1101 buildable_count = buildable.len(),
1102 skipped_count = skipped.len(),
1103 "Resolution complete"
1104 );
1105
1106 for pkgname in buildable.keys() {
1108 debug!(pkgname = %pkgname.pkgname(), "Package is buildable");
1109 }
1110
1111 let scan_failed: Vec<ScanFailure> = self
1113 .scan_failures
1114 .iter()
1115 .map(|(pkgpath, error)| ScanFailure {
1116 pkgpath: pkgpath.clone(),
1117 error: error.clone(),
1118 })
1119 .collect();
1120
1121 let result =
1122 ScanResult { buildable, skipped, scan_failed, all_ordered };
1123
1124 if !errors.is_empty() {
1126 for err in &errors {
1127 error!(error = %err, "Unresolved dependency");
1128 }
1129 bail!("Unresolved dependencies:\n {}", errors.join("\n "));
1130 }
1131
1132 if let Some(err) = cycle_error {
1133 bail!(err);
1134 }
1135
1136 Ok(result)
1137 }
1138}
1139
1140pub fn find_cycle<'a>(
1141 graph: &'a DiGraphMap<&'a str, ()>,
1142) -> Option<Vec<&'a str>> {
1143 let mut visited = HashSet::new();
1144 let mut in_stack = HashSet::new();
1145 let mut stack = Vec::new();
1146
1147 for node in graph.nodes() {
1148 if visited.contains(&node) {
1149 continue;
1150 }
1151 if let Some(cycle) =
1152 dfs(graph, node, &mut visited, &mut stack, &mut in_stack)
1153 {
1154 return Some(cycle);
1155 }
1156 }
1157 None
1158}
1159
1160fn dfs<'a>(
1161 graph: &'a DiGraphMap<&'a str, ()>,
1162 node: &'a str,
1163 visited: &mut HashSet<&'a str>,
1164 stack: &mut Vec<&'a str>,
1165 in_stack: &mut HashSet<&'a str>,
1166) -> Option<Vec<&'a str>> {
1167 visited.insert(node);
1168 stack.push(node);
1169 in_stack.insert(node);
1170 for neighbor in graph.neighbors(node) {
1171 if in_stack.contains(neighbor) {
1172 if let Some(pos) = stack.iter().position(|&n| n == neighbor) {
1173 return Some(stack[pos..].to_vec());
1174 }
1175 } else if !visited.contains(neighbor) {
1176 let cycle = dfs(graph, neighbor, visited, stack, in_stack);
1177 if cycle.is_some() {
1178 return cycle;
1179 }
1180 }
1181 }
1182 stack.pop();
1183 in_stack.remove(node);
1184 None
1185}