1use crate::tui::MultiProgress;
64use crate::{Config, RunContext, Sandbox};
65use anyhow::{Context, Result, bail};
66use petgraph::graphmap::DiGraphMap;
67use pkgsrc::{Depend, PkgName, PkgPath, ScanIndex};
68use rayon::prelude::*;
69use std::collections::{HashMap, HashSet};
70use std::io::BufReader;
71use std::path::Path;
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}
104
105#[derive(Clone, Debug)]
107pub struct ScanFailure {
108 pub pkgpath: PkgPath,
110 pub error: String,
112}
113
114#[derive(Clone, Debug)]
118pub struct ResolvedIndex {
119 pub index: ScanIndex,
121 pub depends: Vec<PkgName>,
123}
124
125impl ResolvedIndex {
126 pub fn from_scan_index(index: ScanIndex) -> Self {
128 Self { index, depends: Vec::new() }
129 }
130}
131
132impl std::ops::Deref for ResolvedIndex {
133 type Target = ScanIndex;
134 fn deref(&self) -> &Self::Target {
135 &self.index
136 }
137}
138
139impl std::ops::DerefMut for ResolvedIndex {
140 fn deref_mut(&mut self) -> &mut Self::Target {
141 &mut self.index
142 }
143}
144
145impl std::fmt::Display for ResolvedIndex {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write!(f, "{}", self.index)?;
148 write!(f, "DEPENDS=")?;
149 for (i, d) in self.depends.iter().enumerate() {
150 if i > 0 {
151 write!(f, " ")?;
152 }
153 write!(f, "{d}")?;
154 }
155 writeln!(f)
156 }
157}
158
159#[derive(Clone, Debug, Default)]
164pub struct ScanResult {
165 pub buildable: HashMap<PkgName, ResolvedIndex>,
169 pub skipped: Vec<SkippedPackage>,
171 pub scan_failed: Vec<ScanFailure>,
173}
174
175impl ScanResult {
176 pub fn write_resolve_log(
178 &self,
179 path: &std::path::Path,
180 ) -> anyhow::Result<()> {
181 let mut out = String::new();
182
183 let mut pkgnames: Vec<_> = self.buildable.keys().collect();
185 pkgnames.sort_by(|a, b| a.pkgname().cmp(b.pkgname()));
186
187 for pkgname in pkgnames {
188 let idx = &self.buildable[pkgname];
189 out.push_str(&idx.to_string());
190 out.push('\n');
191 }
192
193 for pkg in &self.skipped {
195 out.push_str(&format!("PKGNAME={}\n", pkg.pkgname));
196 if let Some(ref loc) = pkg.pkgpath {
197 out.push_str(&format!(
198 "PKG_LOCATION={}\n",
199 loc.as_path().display()
200 ));
201 }
202 match &pkg.reason {
203 SkipReason::PkgSkipReason(r) => {
204 out.push_str(&format!("PKG_SKIP_REASON={}\n", r));
205 }
206 SkipReason::PkgFailReason(r) => {
207 out.push_str(&format!("PKG_FAIL_REASON={}\n", r));
208 }
209 }
210 out.push('\n');
211 }
212
213 std::fs::write(path, &out)?;
214 Ok(())
215 }
216
217 pub fn write_resolve_dag(
219 &self,
220 path: &std::path::Path,
221 ) -> anyhow::Result<()> {
222 let mut edges: Vec<String> = Vec::new();
223
224 for (pkgname, idx) in &self.buildable {
225 for dep in &idx.depends {
226 edges.push(format!("{} -> {}", dep, pkgname));
227 }
228 }
229
230 edges.sort();
232
233 let out = edges.join("\n") + "\n";
234 std::fs::write(path, &out)?;
235 Ok(())
236 }
237}
238
239#[derive(Debug, Default)]
272pub struct Scan {
273 config: Config,
274 sandbox: Sandbox,
275 incoming: HashSet<PkgPath>,
276 done: HashMap<PkgPath, Vec<ScanIndex>>,
277 resolved: HashMap<PkgName, ResolvedIndex>,
278 full_tree: bool,
280 scan_failures: Vec<(PkgPath, String)>,
282}
283
284impl Scan {
285 pub fn new(config: &Config) -> Scan {
286 let sandbox = Sandbox::new(config);
287 debug!(pkgsrc = %config.pkgsrc().display(),
288 make = %config.make().display(),
289 scan_threads = config.scan_threads(),
290 "Created new Scan instance"
291 );
292 Scan { config: config.clone(), sandbox, ..Default::default() }
293 }
294
295 pub fn add(&mut self, pkgpath: &PkgPath) {
296 info!(pkgpath = %pkgpath.as_path().display(), "Adding package to scan queue");
297 self.incoming.insert(pkgpath.clone());
298 }
299
300 fn discover_packages(&mut self) -> anyhow::Result<()> {
302 println!("Discovering packages...");
303 self.full_tree = true;
304 let pkgsrc = self.config.pkgsrc().display();
305 let make = self.config.make().display();
306
307 let script = format!(
309 "cd {} && {} show-subdir-var VARNAME=SUBDIR\n",
310 pkgsrc, make
311 );
312 let child = self.sandbox.execute_script(0, &script, vec![])?;
313 let output = child
314 .wait_with_output()
315 .context("Failed to run show-subdir-var")?;
316
317 if !output.status.success() {
318 let stderr = String::from_utf8_lossy(&output.stderr);
319 bail!("Failed to get categories: {}", stderr);
320 }
321
322 let stdout = String::from_utf8_lossy(&output.stdout);
323 let entries: Vec<&str> = stdout.split_whitespace().collect();
324
325 for entry in entries {
326 if entry.contains('/') {
327 if let Ok(pkgpath) = PkgPath::new(entry) {
329 self.incoming.insert(pkgpath);
330 }
331 } else {
332 let script = format!(
334 "cd {}/{} && {} show-subdir-var VARNAME=SUBDIR\n",
335 pkgsrc, entry, make
336 );
337 let child = self.sandbox.execute_script(0, &script, vec![])?;
338 let cat_output = child.wait_with_output();
339
340 match cat_output {
341 Ok(o) if o.status.success() => {
342 let pkgs = String::from_utf8_lossy(&o.stdout);
343 for pkg in pkgs.split_whitespace() {
344 let path = format!("{}/{}", entry, pkg);
345 if let Ok(pkgpath) = PkgPath::new(&path) {
346 self.incoming.insert(pkgpath);
347 }
348 }
349 }
350 Ok(o) => {
351 let stderr = String::from_utf8_lossy(&o.stderr);
352 debug!(category = entry, stderr = %stderr,
353 "Failed to get packages for category");
354 }
355 Err(e) => {
356 debug!(category = entry, error = %e,
357 "Failed to run make in category");
358 }
359 }
360 }
361 }
362
363 info!(discovered = self.incoming.len(), "Package discovery complete");
364 println!("Discovered {} packages", self.incoming.len());
365
366 Ok(())
367 }
368
369 pub fn start(&mut self, ctx: &RunContext) -> anyhow::Result<bool> {
370 info!(
371 incoming_count = self.incoming.len(),
372 sandbox_enabled = self.sandbox.enabled(),
373 "Starting package scan"
374 );
375
376 let pool = rayon::ThreadPoolBuilder::new()
377 .num_threads(self.config.scan_threads())
378 .build()
379 .context("Failed to build scan thread pool")?;
380
381 let shutdown_flag = Arc::clone(&ctx.shutdown);
382 let stats = ctx.stats.clone();
383
384 let script_envs = self.config.script_env();
389
390 if self.sandbox.enabled() {
391 println!("Creating sandbox...");
392 if let Err(e) = self.sandbox.create(0) {
393 if let Err(destroy_err) = self.sandbox.destroy(0) {
394 eprintln!(
395 "Warning: failed to destroy sandbox: {}",
396 destroy_err
397 );
398 }
399 return Err(e);
400 }
401
402 if let Some(pre_build) = self.config.script("pre-build") {
404 debug!("Running pre-build script");
405 let child = self.sandbox.execute(
406 0,
407 pre_build,
408 script_envs.clone(),
409 None,
410 None,
411 )?;
412 let output = child
413 .wait_with_output()
414 .context("Failed to wait for pre-build")?;
415 if !output.status.success() {
416 let stderr = String::from_utf8_lossy(&output.stderr);
417 error!(exit_code = ?output.status.code(), stderr = %stderr, "pre-build script failed");
418 }
419 }
420 }
421
422 if self.incoming.is_empty() {
424 self.discover_packages()?;
425 }
426
427 println!("Scanning packages...");
428
429 let progress = Arc::new(Mutex::new(
431 MultiProgress::new(
432 "Scanning",
433 "Scanned",
434 self.incoming.len(),
435 self.config.scan_threads(),
436 false,
437 )
438 .expect("Failed to initialize progress display"),
439 ));
440
441 let stop_refresh = Arc::new(AtomicBool::new(false));
443
444 let progress_refresh = Arc::clone(&progress);
446 let stop_flag = Arc::clone(&stop_refresh);
447 let shutdown_for_refresh = Arc::clone(&shutdown_flag);
448 let refresh_thread = std::thread::spawn(move || {
449 while !stop_flag.load(Ordering::Relaxed)
450 && !shutdown_for_refresh.load(Ordering::SeqCst)
451 {
452 if let Ok(mut p) = progress_refresh.lock() {
453 let _ = p.poll_events();
455 let _ = p.render();
456 }
457 std::thread::sleep(Duration::from_millis(50));
458 }
459 });
460
461 let mut interrupted = false;
467 loop {
468 if shutdown_flag.load(Ordering::Relaxed) {
470 stop_refresh.store(true, Ordering::Relaxed);
472 if let Ok(mut p) = progress.lock() {
473 let _ = p.finish_interrupted();
474 }
475 interrupted = true;
476 break;
477 }
478
479 let mut parpaths: Vec<(PkgPath, Result<Vec<ScanIndex>>)> = vec![];
483 for pkgpath in &self.incoming {
484 parpaths.push((pkgpath.clone(), Ok(vec![])));
485 }
486
487 let progress_clone = Arc::clone(&progress);
488 let shutdown_clone = Arc::clone(&shutdown_flag);
489 let stats_clone = stats.clone();
490 pool.install(|| {
491 parpaths.par_iter_mut().for_each(|pkg| {
492 if shutdown_clone.load(Ordering::Relaxed) {
494 return;
495 }
496
497 let (pkgpath, result) = pkg;
498 let pathname =
499 pkgpath.as_path().to_string_lossy().to_string();
500
501 let thread_id = rayon::current_thread_index().unwrap_or(0);
503
504 if let Ok(mut p) = progress_clone.lock() {
506 p.state_mut().set_worker_active(thread_id, &pathname);
507 }
508
509 let scan_start = Instant::now();
510 *result = self.scan_pkgpath(pkgpath);
511 let scan_duration = scan_start.elapsed();
512
513 if let Some(ref s) = stats_clone {
515 s.scan(&pathname, scan_duration, result.is_ok());
516 }
517
518 if let Ok(mut p) = progress_clone.lock() {
520 p.state_mut().set_worker_idle(thread_id);
521 if result.is_ok() {
522 p.state_mut().increment_completed();
523 } else {
524 p.state_mut().increment_failed();
525 }
526 }
527 });
528 });
529
530 if shutdown_flag.load(Ordering::Relaxed) {
532 stop_refresh.store(true, Ordering::Relaxed);
534 if let Ok(mut p) = progress.lock() {
535 let _ = p.finish_interrupted();
536 }
537 interrupted = true;
538 break;
539 }
540
541 let mut new_incoming: HashSet<PkgPath> = HashSet::new();
547 for (pkgpath, scanpkgs) in parpaths.drain(..) {
548 let scanpkgs = match scanpkgs {
549 Ok(pkgs) => pkgs,
550 Err(e) => {
551 self.scan_failures
552 .push((pkgpath.clone(), format!("{}", e)));
553 self.done.insert(pkgpath.clone(), vec![]);
554 continue;
555 }
556 };
557 self.done.insert(pkgpath.clone(), scanpkgs.clone());
558 if !self.full_tree {
561 for pkg in scanpkgs {
562 if let Some(ref all_deps) = pkg.all_depends {
563 for dep in all_deps {
564 if !self.done.contains_key(dep.pkgpath())
565 && !self.incoming.contains(dep.pkgpath())
566 && new_incoming
567 .insert(dep.pkgpath().clone())
568 {
569 if let Ok(mut p) = progress.lock() {
571 p.state_mut().total += 1;
572 }
573 }
574 }
575 }
576 }
577 }
578 }
579
580 self.incoming = new_incoming;
586 if self.incoming.is_empty() {
587 break;
588 }
589 }
590
591 stop_refresh.store(true, Ordering::Relaxed);
593 let _ = refresh_thread.join();
594
595 if !interrupted {
598 if let Ok(mut p) = progress.lock() {
599 let _ = p.finish();
600 }
601 }
602
603 if self.sandbox.enabled() {
604 if let Some(post_build) = self.config.script("post-build") {
606 debug!("Running post-build script");
607 let child = self.sandbox.execute(
608 0,
609 post_build,
610 script_envs,
611 None,
612 None,
613 )?;
614 let output = child
615 .wait_with_output()
616 .context("Failed to wait for post-build")?;
617 if !output.status.success() {
618 let stderr = String::from_utf8_lossy(&output.stderr);
619 error!(exit_code = ?output.status.code(), stderr = %stderr, "post-build script failed");
620 }
621 }
622
623 self.sandbox.destroy(0)?;
624 }
625
626 if interrupted {
627 return Ok(true);
628 }
629
630 Ok(false)
631 }
632
633 pub fn scan_errors(&self) -> Vec<String> {
635 self.scan_failures.iter().map(|(_, e)| e.clone()).collect()
636 }
637
638 pub fn scan_failures(&self) -> &[(PkgPath, String)] {
640 &self.scan_failures
641 }
642
643 pub fn scan_pkgpath(
648 &self,
649 pkgpath: &PkgPath,
650 ) -> anyhow::Result<Vec<ScanIndex>> {
651 let pkgpath_str = pkgpath.as_path().display().to_string();
652 debug!(pkgpath = %pkgpath_str, "Scanning package");
653
654 let bmake = self.config.make().display().to_string();
655 let pkgsrcdir = self.config.pkgsrc().display().to_string();
656 let script = format!(
657 "cd {}/{} && {} pbulk-index\n",
658 pkgsrcdir, pkgpath_str, bmake
659 );
660
661 let scan_env = self.config.scan_env();
662 trace!(pkgpath = %pkgpath_str,
663 script = %script,
664 scan_env = ?scan_env,
665 "Executing pkg-scan"
666 );
667 let child = self.sandbox.execute_script(0, &script, scan_env)?;
668 let output = child.wait_with_output()?;
669
670 if !output.status.success() {
671 let stderr = String::from_utf8_lossy(&output.stderr);
672 error!(pkgpath = %pkgpath_str,
673 exit_code = ?output.status.code(),
674 stderr = %stderr,
675 "pkg-scan script failed"
676 );
677 let stderr = stderr.trim();
678 let msg = if stderr.is_empty() {
679 format!("Scan failed for {}", pkgpath_str)
680 } else {
681 format!("Scan failed for {}: {}", pkgpath_str, stderr)
682 };
683 bail!(msg);
684 }
685
686 let stdout_str = String::from_utf8_lossy(&output.stdout);
687 trace!(pkgpath = %pkgpath_str,
688 stdout_len = stdout_str.len(),
689 stdout = %stdout_str,
690 "pkg-scan script output"
691 );
692
693 let reader = BufReader::new(&output.stdout[..]);
694 let mut index: Vec<ScanIndex> =
695 ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
696
697 info!(pkgpath = %pkgpath_str,
698 packages_found = index.len(),
699 "Scan complete for pkgpath"
700 );
701
702 for pkg in &mut index {
706 pkg.pkg_location = Some(pkgpath.clone());
707 debug!(pkgpath = %pkgpath_str,
708 pkgname = %pkg.pkgname.pkgname(),
709 skip_reason = ?pkg.pkg_skip_reason,
710 fail_reason = ?pkg.pkg_fail_reason,
711 depends_count = pkg.all_depends.as_ref().map_or(0, |v| v.len()),
712 "Found package in scan"
713 );
714 }
715
716 Ok(index)
717 }
718
719 pub fn scanned(&self) -> impl Iterator<Item = &ScanIndex> {
721 self.done.values().flatten()
722 }
723
724 pub fn write_log(&self, path: &std::path::Path) -> anyhow::Result<()> {
726 let mut out = String::new();
727 for idx in self.scanned() {
728 out.push_str(&idx.to_string());
729 out.push('\n');
730 }
731 std::fs::write(path, &out)?;
732 Ok(())
733 }
734
735 pub fn resolve(&mut self, log_dir: Option<&Path>) -> Result<ScanResult> {
746 info!(
747 done_pkgpaths = self.done.len(),
748 "Starting dependency resolution"
749 );
750
751 let mut pkgnames: HashSet<PkgName> = HashSet::new();
764 let mut skipped: Vec<SkippedPackage> = Vec::new();
765
766 for (pkgpath, index) in &self.done {
768 debug!(pkgpath = %pkgpath.as_path().display(),
769 packages_in_index = index.len(),
770 "Processing done entry"
771 );
772 }
773
774 for index in self.done.values() {
775 for pkg in index {
776 if let Some(reason) = &pkg.pkg_skip_reason {
778 if !reason.is_empty() {
779 info!(pkgname = %pkg.pkgname.pkgname(),
780 reason = %reason,
781 "Skipping package due to PKG_SKIP_REASON"
782 );
783 skipped.push(SkippedPackage {
784 pkgname: pkg.pkgname.clone(),
785 pkgpath: pkg.pkg_location.clone(),
786 reason: SkipReason::PkgSkipReason(reason.clone()),
787 });
788 continue;
789 }
790 }
791 if let Some(reason) = &pkg.pkg_fail_reason {
792 if !reason.is_empty() {
793 info!(pkgname = %pkg.pkgname.pkgname(),
794 reason = %reason,
795 "Skipping package due to PKG_FAIL_REASON"
796 );
797 skipped.push(SkippedPackage {
798 pkgname: pkg.pkgname.clone(),
799 pkgpath: pkg.pkg_location.clone(),
800 reason: SkipReason::PkgFailReason(reason.clone()),
801 });
802 continue;
803 }
804 }
805
806 if pkgnames.contains(&pkg.pkgname) {
809 debug!(pkgname = %pkg.pkgname.pkgname(),
810 multi_version = ?pkg.multi_version,
811 "Skipping duplicate PKGNAME"
812 );
813 continue;
814 }
815
816 debug!(pkgname = %pkg.pkgname.pkgname(),
817 "Adding package to resolved set"
818 );
819 pkgnames.insert(pkg.pkgname.clone());
820 self.resolved.insert(
821 pkg.pkgname.clone(),
822 ResolvedIndex::from_scan_index(pkg.clone()),
823 );
824 }
825 }
826
827 info!(
828 resolved_count = self.resolved.len(),
829 skipped_count = skipped.len(),
830 "Initial resolution complete"
831 );
832
833 if !skipped.is_empty() {
834 println!(
835 "Skipping {} packages with PKG_SKIP_REASON or PKG_FAIL_REASON",
836 skipped.len()
837 );
838 }
839
840 let skipped_pkgnames: HashSet<PkgName> =
845 skipped.iter().map(|s| s.pkgname.clone()).collect();
846
847 let mut match_cache: HashMap<Depend, PkgName> = HashMap::new();
852
853 let mut skip_due_to_dep: HashMap<PkgName, String> = HashMap::new();
858 let mut errors: Vec<String> = Vec::new();
859
860 for pkg in self.resolved.values_mut() {
861 let all_deps = match pkg.all_depends.clone() {
862 Some(deps) => deps,
863 None => continue,
864 };
865 for depend in &all_deps {
866 if let Some(pkgname) = match_cache.get(depend) {
870 pkg.depends.push(pkgname.clone());
871 continue;
872 }
873 let mut best: Option<&PkgName> = None;
877 for candidate in &pkgnames {
878 if depend.pattern().matches(candidate.pkgname()) {
879 if let Some(current) = best {
880 best = match depend.pattern().best_match(
881 current.pkgname(),
882 candidate.pkgname(),
883 ) {
884 Ok(Some(m)) if m == current.pkgname() => {
885 Some(current)
886 }
887 Ok(Some(m)) if m == candidate.pkgname() => {
888 Some(candidate)
889 }
890 Ok(Some(_)) => todo!(),
891 Ok(None) | Err(_) => None,
892 };
893 } else {
894 best = Some(candidate);
895 }
896 }
897 }
898 if let Some(pkgname) = best {
903 pkg.depends.push(pkgname.clone());
904 match_cache.insert(depend.clone(), pkgname.clone());
905 } else {
906 let mut matched_skipped: Option<&PkgName> = None;
908 for candidate in &skipped_pkgnames {
909 if depend.pattern().matches(candidate.pkgname()) {
910 matched_skipped = Some(candidate);
911 break;
912 }
913 }
914
915 if let Some(skipped_dep) = matched_skipped {
916 skip_due_to_dep.insert(
918 pkg.index.pkgname.clone(),
919 format!(
920 "Dependency {} skipped",
921 skipped_dep.pkgname()
922 ),
923 );
924 } else {
925 errors.push(format!(
927 "No match found for {} in {}",
928 depend.pattern().pattern(),
929 pkg.index.pkgname.pkgname()
930 ));
931 }
932 }
933 }
934 }
935
936 loop {
941 let mut new_skips: HashMap<PkgName, String> = HashMap::new();
942
943 for pkg in self.resolved.values() {
944 if skip_due_to_dep.contains_key(&pkg.pkgname) {
945 continue;
946 }
947 for dep in &pkg.depends {
948 if skip_due_to_dep.contains_key(dep) {
949 new_skips.insert(
951 pkg.pkgname.clone(),
952 format!("Dependency {} skipped", dep.pkgname()),
953 );
954 break;
955 }
956 }
957 }
958
959 if new_skips.is_empty() {
960 break;
961 }
962 skip_due_to_dep.extend(new_skips);
963 }
964
965 for (pkgname, reason) in &skip_due_to_dep {
969 if let Some(pkg) = self.resolved.remove(pkgname) {
970 skipped.push(SkippedPackage {
971 pkgname: pkg.pkgname.clone(),
972 pkgpath: pkg.pkg_location.clone(),
973 reason: SkipReason::PkgSkipReason(reason.clone()),
974 });
975 }
976 }
977
978 let errors: Vec<String> = errors
984 .into_iter()
985 .filter(|err| {
986 if let Some(pkgname_str) = err.split(" in ").last() {
989 !skip_due_to_dep.keys().any(|k| k.pkgname() == pkgname_str)
990 } else {
991 true }
993 })
994 .collect();
995
996 debug!(
1000 resolved_count = self.resolved.len(),
1001 "Checking for circular dependencies"
1002 );
1003 let mut graph = DiGraphMap::new();
1004 for (pkgname, index) in &self.resolved {
1005 for dep in &index.depends {
1006 graph.add_edge(dep.pkgname(), pkgname.pkgname(), ());
1007 }
1008 }
1009 let cycle_error = find_cycle(&graph).map(|cycle| {
1010 let mut err = "Circular dependencies detected:\n".to_string();
1011 for n in cycle.iter().rev() {
1012 err.push_str(&format!("\t{}\n", n));
1013 }
1014 err.push_str(&format!("\t{}", cycle.last().unwrap()));
1015 error!(cycle = ?cycle, "Circular dependency detected");
1016 err
1017 });
1018
1019 info!(
1020 buildable_count = self.resolved.len(),
1021 skipped_count = skipped.len(),
1022 "Resolution complete"
1023 );
1024
1025 for pkgname in self.resolved.keys() {
1027 debug!(pkgname = %pkgname.pkgname(), "Package is buildable");
1028 }
1029
1030 let scan_failed: Vec<ScanFailure> = self
1032 .scan_failures
1033 .iter()
1034 .map(|(pkgpath, error)| ScanFailure {
1035 pkgpath: pkgpath.clone(),
1036 error: error.clone(),
1037 })
1038 .collect();
1039
1040 let result = ScanResult {
1041 buildable: self.resolved.clone(),
1042 skipped,
1043 scan_failed,
1044 };
1045
1046 if let Some(dir) = log_dir {
1048 let _ = result.write_resolve_log(&dir.join("resolve.log"));
1049 let _ = result.write_resolve_dag(&dir.join("resolve.dag"));
1050 }
1051
1052 if !errors.is_empty() {
1054 for err in &errors {
1055 error!(error = %err, "Unresolved dependency");
1056 }
1057 bail!("Unresolved dependencies:\n {}", errors.join("\n "));
1058 }
1059
1060 if let Some(err) = cycle_error {
1061 bail!(err);
1062 }
1063
1064 Ok(result)
1065 }
1066}
1067
1068pub fn find_cycle<'a>(
1069 graph: &'a DiGraphMap<&'a str, ()>,
1070) -> Option<Vec<&'a str>> {
1071 let mut visited = HashSet::new();
1072 let mut in_stack = HashSet::new();
1073 let mut stack = Vec::new();
1074
1075 for node in graph.nodes() {
1076 if visited.contains(&node) {
1077 continue;
1078 }
1079 let cycle = dfs(graph, node, &mut visited, &mut stack, &mut in_stack);
1080 if cycle.is_some() {
1081 return cycle;
1082 }
1083 }
1084 None
1085}
1086
1087fn dfs<'a>(
1088 graph: &'a DiGraphMap<&'a str, ()>,
1089 node: &'a str,
1090 visited: &mut HashSet<&'a str>,
1091 stack: &mut Vec<&'a str>,
1092 in_stack: &mut HashSet<&'a str>,
1093) -> Option<Vec<&'a str>> {
1094 visited.insert(node);
1095 stack.push(node);
1096 in_stack.insert(node);
1097 for neighbor in graph.neighbors(node) {
1098 if in_stack.contains(neighbor) {
1099 if let Some(pos) = stack.iter().position(|&n| n == neighbor) {
1100 return Some(stack[pos..].to_vec());
1101 }
1102 } else if !visited.contains(neighbor) {
1103 let cycle = dfs(graph, neighbor, visited, stack, in_stack);
1104 if cycle.is_some() {
1105 return cycle;
1106 }
1107 }
1108 }
1109 stack.pop();
1110 in_stack.remove(node);
1111 None
1112}