1use crate::config::PkgsrcEnv;
41use crate::sandbox::{SingleSandboxScope, wait_output_with_shutdown};
42use crate::tui::{MultiProgress, REFRESH_INTERVAL, format_duration};
43use crate::{Config, RunContext, Sandbox};
44use anyhow::{Context, Result, bail};
45use crossterm::event;
46use indexmap::IndexMap;
47use petgraph::graphmap::DiGraphMap;
48use pkgsrc::{Depend, PkgName, PkgPath, ScanIndex};
49use rayon::prelude::*;
50use std::collections::{HashMap, HashSet};
51use std::io::BufReader;
52use std::sync::atomic::{AtomicBool, Ordering};
53use std::sync::{Arc, Mutex};
54use tracing::{debug, error, info, info_span, trace, warn};
55
56#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub enum SkipReason {
59 PkgSkip(String),
61 PkgFail(String),
63 IndirectSkip(String),
65 IndirectFail(String),
67 UnresolvedDep(String),
69}
70
71impl SkipReason {
72 pub fn status(&self) -> &'static str {
74 match self {
75 SkipReason::PkgSkip(_) => "pre-skipped",
76 SkipReason::PkgFail(_) => "pre-failed",
77 SkipReason::IndirectSkip(_) => "indirect-skipped",
78 SkipReason::IndirectFail(_) => "indirect-failed",
79 SkipReason::UnresolvedDep(_) => "unresolved",
80 }
81 }
82
83 pub fn is_direct(&self) -> bool {
85 matches!(
86 self,
87 SkipReason::PkgSkip(_)
88 | SkipReason::PkgFail(_)
89 | SkipReason::UnresolvedDep(_)
90 )
91 }
92}
93
94impl std::fmt::Display for SkipReason {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 SkipReason::PkgSkip(r)
98 | SkipReason::PkgFail(r)
99 | SkipReason::IndirectSkip(r)
100 | SkipReason::IndirectFail(r) => write!(f, "{}", r),
101 SkipReason::UnresolvedDep(p) => {
102 write!(f, "Could not resolve: {}", p)
103 }
104 }
105 }
106}
107
108#[derive(Clone, Debug, Default)]
110pub struct SkippedCounts {
111 pub pkg_skip: usize,
113 pub pkg_fail: usize,
115 pub unresolved: usize,
117 pub indirect_skip: usize,
119 pub indirect_fail: usize,
121}
122
123#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
125pub struct ResolvedPackage {
126 pub index: ScanIndex,
128 pub pkgpath: PkgPath,
130 pub resolved_depends: Vec<PkgName>,
132}
133
134impl ResolvedPackage {
135 pub fn pkgname(&self) -> &PkgName {
137 &self.index.pkgname
138 }
139
140 pub fn depends(&self) -> &[PkgName] {
142 &self.resolved_depends
143 }
144
145 pub fn bootstrap_pkg(&self) -> Option<&str> {
147 self.index.bootstrap_pkg.as_deref()
148 }
149
150 pub fn usergroup_phase(&self) -> Option<&str> {
152 self.index.usergroup_phase.as_deref()
153 }
154
155 pub fn multi_version(&self) -> Option<&[String]> {
157 self.index.multi_version.as_deref()
158 }
159
160 pub fn pbulk_weight(&self) -> Option<&str> {
162 self.index.pbulk_weight.as_deref()
163 }
164}
165
166impl std::fmt::Display for ResolvedPackage {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 write!(f, "{}", self.index)?;
169 if !self.resolved_depends.is_empty() {
170 write!(f, "DEPENDS=")?;
171 for (i, d) in self.resolved_depends.iter().enumerate() {
172 if i > 0 {
173 write!(f, " ")?;
174 }
175 write!(f, "{d}")?;
176 }
177 writeln!(f)?;
178 }
179 Ok(())
180 }
181}
182
183#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
185pub enum ScanResult {
186 Buildable(ResolvedPackage),
188 Skipped {
190 pkgpath: PkgPath,
192 reason: SkipReason,
194 index: Option<ScanIndex>,
196 resolved_depends: Vec<PkgName>,
198 },
199 ScanFail {
201 pkgpath: PkgPath,
203 error: String,
205 },
206}
207
208impl ScanResult {
209 pub fn pkgpath(&self) -> &PkgPath {
211 match self {
212 ScanResult::Buildable(pkg) => &pkg.pkgpath,
213 ScanResult::Skipped { pkgpath, .. } => pkgpath,
214 ScanResult::ScanFail { pkgpath, .. } => pkgpath,
215 }
216 }
217
218 pub fn pkgname(&self) -> Option<&PkgName> {
220 match self {
221 ScanResult::Buildable(pkg) => Some(pkg.pkgname()),
222 ScanResult::Skipped { index, .. } => {
223 index.as_ref().map(|i| &i.pkgname)
224 }
225 ScanResult::ScanFail { .. } => None,
226 }
227 }
228
229 pub fn is_buildable(&self) -> bool {
231 matches!(self, ScanResult::Buildable(_))
232 }
233
234 pub fn as_buildable(&self) -> Option<&ResolvedPackage> {
236 match self {
237 ScanResult::Buildable(pkg) => Some(pkg),
238 _ => None,
239 }
240 }
241
242 pub fn depends(&self) -> &[PkgName] {
244 match self {
245 ScanResult::Buildable(pkg) => &pkg.resolved_depends,
246 ScanResult::Skipped { resolved_depends, .. } => resolved_depends,
247 ScanResult::ScanFail { .. } => &[],
248 }
249 }
250}
251
252impl std::fmt::Display for ScanResult {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 ScanResult::Buildable(pkg) => write!(f, "{}", pkg),
256 ScanResult::Skipped {
257 index,
258 pkgpath,
259 reason,
260 resolved_depends,
261 } => {
262 if let Some(idx) = index {
263 write!(f, "{}", idx)?;
264 if !matches!(reason, SkipReason::UnresolvedDep(_))
266 && !resolved_depends.is_empty()
267 {
268 write!(f, "DEPENDS=")?;
269 for (i, d) in resolved_depends.iter().enumerate() {
270 if i > 0 {
271 write!(f, " ")?;
272 }
273 write!(f, "{d}")?;
274 }
275 writeln!(f)?;
276 }
277 } else {
278 writeln!(f, "PKGPATH={}", pkgpath)?;
279 }
280 Ok(())
281 }
282 ScanResult::ScanFail { pkgpath, .. } => {
283 writeln!(f, "PKGPATH={}", pkgpath)
284 }
285 }
286 }
287}
288
289#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
293pub struct ScanSummary {
294 pub pkgpaths: usize,
296 pub packages: Vec<ScanResult>,
298}
299
300#[derive(Clone, Debug, Default)]
302pub struct ScanCounts {
303 pub buildable: usize,
305 pub skipped: SkippedCounts,
307 pub scanfail: usize,
309}
310
311impl ScanSummary {
312 pub fn counts(&self) -> ScanCounts {
314 let mut c = ScanCounts::default();
315 for p in &self.packages {
316 match p {
317 ScanResult::Buildable(_) => c.buildable += 1,
318 ScanResult::Skipped {
319 reason: SkipReason::PkgSkip(_), ..
320 } => c.skipped.pkg_skip += 1,
321 ScanResult::Skipped {
322 reason: SkipReason::PkgFail(_), ..
323 } => c.skipped.pkg_fail += 1,
324 ScanResult::Skipped {
325 reason: SkipReason::IndirectSkip(_),
326 ..
327 } => c.skipped.indirect_skip += 1,
328 ScanResult::Skipped {
329 reason: SkipReason::IndirectFail(_),
330 ..
331 } => c.skipped.indirect_fail += 1,
332 ScanResult::Skipped {
333 reason: SkipReason::UnresolvedDep(_),
334 ..
335 } => c.skipped.unresolved += 1,
336 ScanResult::ScanFail { .. } => c.scanfail += 1,
337 }
338 }
339 c
340 }
341
342 pub fn buildable(&self) -> impl Iterator<Item = &ResolvedPackage> {
344 self.packages.iter().filter_map(|p| p.as_buildable())
345 }
346
347 pub fn failed(&self) -> impl Iterator<Item = &ScanResult> {
349 self.packages.iter().filter(|p| !p.is_buildable())
350 }
351
352 pub fn count_buildable(&self) -> usize {
354 self.packages.iter().filter(|p| p.is_buildable()).count()
355 }
356
357 pub fn errors(&self) -> impl Iterator<Item = &str> {
359 self.packages.iter().filter_map(|p| match p {
360 ScanResult::ScanFail { error, .. } => Some(error.as_str()),
361 ScanResult::Skipped {
362 reason: SkipReason::UnresolvedDep(e),
363 ..
364 } => Some(e.as_str()),
365 _ => None,
366 })
367 }
368}
369
370impl std::fmt::Display for ScanSummary {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 let c = self.counts();
373 let s = &c.skipped;
374 write!(
375 f,
376 "Resolved {} total packages from {} package paths\n{} buildable, {} pre-skipped, {} pre-failed, {} unresolved",
377 self.packages.len(),
378 self.pkgpaths,
379 c.buildable,
380 s.pkg_skip + s.indirect_skip,
381 s.pkg_fail + s.indirect_fail,
382 s.unresolved
383 )
384 }
385}
386
387#[derive(Debug, Default)]
389pub struct Scan {
390 config: Config,
391 sandbox: Sandbox,
392 incoming: HashSet<PkgPath>,
393 done: HashSet<PkgPath>,
395 initial_cached: usize,
397 discovered_cached: usize,
399 packages: IndexMap<PkgName, ScanIndex>,
401 full_tree: bool,
404 full_scan_complete: bool,
406 scan_failures: Vec<(PkgPath, String)>,
408 pkgsrc_env: Option<PkgsrcEnv>,
410}
411
412impl Scan {
413 pub fn new(config: &Config) -> Scan {
414 let sandbox = Sandbox::new(config);
415 debug!(pkgsrc = %config.pkgsrc().display(),
416 make = %config.make().display(),
417 scan_threads = config.scan_threads(),
418 "Created new Scan instance"
419 );
420 Scan {
421 config: config.clone(),
422 sandbox,
423 incoming: HashSet::new(),
424 done: HashSet::new(),
425 initial_cached: 0,
426 discovered_cached: 0,
427 packages: IndexMap::new(),
428 full_tree: true,
429 full_scan_complete: false,
430 scan_failures: Vec::new(),
431 pkgsrc_env: None,
432 }
433 }
434
435 pub fn add(&mut self, pkgpath: &PkgPath) {
436 info!(pkgpath = %pkgpath.as_path().display(), "Adding package to scan queue");
437 self.full_tree = false;
438 self.incoming.insert(pkgpath.clone());
439 }
440
441 pub fn is_full_tree(&self) -> bool {
443 self.full_tree
444 }
445
446 pub fn set_full_scan_complete(&mut self) {
448 self.full_scan_complete = true;
449 }
450
451 pub fn init_from_db(
455 &mut self,
456 db: &crate::db::Database,
457 ) -> Result<(usize, usize)> {
458 let scanned = db.get_scanned_pkgpaths()?;
459 let cached_count = scanned.len();
460 let mut pending_count = 0;
461
462 if cached_count > 0 {
463 info!(
464 cached_count = cached_count,
465 "Found cached scan results in database"
466 );
467
468 if !self.full_tree {
471 self.incoming.retain(|p| !scanned.contains(&p.to_string()));
472 }
473
474 for pkgpath_str in &scanned {
476 if let Ok(pkgpath) = PkgPath::new(pkgpath_str) {
477 self.done.insert(pkgpath);
478 }
479 }
480
481 let unscanned = db.get_unscanned_dependencies()?;
484 if !unscanned.is_empty() {
485 info!(
486 unscanned_count = unscanned.len(),
487 "Found unscanned dependencies from interrupted scan"
488 );
489 for pkgpath_str in unscanned {
490 if let Ok(pkgpath) = PkgPath::new(&pkgpath_str) {
491 if !self.done.contains(&pkgpath) {
492 self.incoming.insert(pkgpath);
493 pending_count += 1;
494 }
495 }
496 }
497 }
498 }
499
500 Ok((cached_count, pending_count))
501 }
502
503 fn discover_packages(
505 &mut self,
506 pool: &rayon::ThreadPool,
507 shutdown: &AtomicBool,
508 ) -> anyhow::Result<()> {
509 println!("Discovering packages...");
510 let pkgsrc = self.config.pkgsrc().display().to_string();
511
512 let child = self.sandbox.execute_command(
514 0,
515 self.config.make(),
516 ["-C", &pkgsrc, "show-subdir-var", "VARNAME=SUBDIR"],
517 vec![],
518 )?;
519 let output = wait_output_with_shutdown(child, shutdown)
520 .context("Failed to run show-subdir-var")?;
521
522 if !output.status.success() {
523 let stderr = String::from_utf8_lossy(&output.stderr);
524 bail!("Failed to get categories: {}", stderr);
525 }
526
527 let stdout = String::from_utf8_lossy(&output.stdout);
528 let entries: Vec<&str> = stdout.split_whitespace().collect();
529
530 let mut categories: Vec<&str> = Vec::new();
532 for entry in entries {
533 if entry.contains('/') {
534 if let Ok(pkgpath) = PkgPath::new(entry) {
535 self.incoming.insert(pkgpath);
536 }
537 } else {
538 categories.push(entry);
539 }
540 }
541
542 let make = self.config.make();
544 let sandbox = &self.sandbox;
545 let discovered: Vec<PkgPath> = pool.install(|| {
546 categories
547 .par_iter()
548 .flat_map(|category| {
549 let workdir = format!("{}/{}", pkgsrc, category);
550 let result = sandbox
551 .execute_command(
552 0,
553 make,
554 [
555 "-C",
556 &workdir,
557 "show-subdir-var",
558 "VARNAME=SUBDIR",
559 ],
560 vec![],
561 )
562 .and_then(|c| wait_output_with_shutdown(c, shutdown));
563
564 match result {
565 Ok(o) if o.status.success() => {
566 let pkgs = String::from_utf8_lossy(&o.stdout);
567 pkgs.split_whitespace()
568 .filter_map(|pkg| {
569 let path = format!("{}/{}", category, pkg);
570 PkgPath::new(&path).ok()
571 })
572 .collect::<Vec<_>>()
573 }
574 Ok(o) => {
575 let stderr = String::from_utf8_lossy(&o.stderr);
576 debug!(category = *category, stderr = %stderr,
577 "Failed to get packages for category");
578 vec![]
579 }
580 Err(e) => {
581 debug!(category = *category, error = %e,
582 "Failed to run make in category");
583 vec![]
584 }
585 }
586 })
587 .collect()
588 });
589
590 self.incoming.extend(discovered);
591
592 info!(discovered = self.incoming.len(), "Package discovery complete");
593 println!("Discovered {} package paths", self.incoming.len());
594
595 Ok(())
596 }
597
598 pub fn start(
599 &mut self,
600 ctx: &RunContext,
601 db: &crate::db::Database,
602 ) -> anyhow::Result<bool> {
603 info!(
604 incoming_count = self.incoming.len(),
605 sandbox_enabled = self.sandbox.enabled(),
606 "Starting package scan"
607 );
608
609 let pool = rayon::ThreadPoolBuilder::new()
610 .num_threads(self.config.scan_threads())
611 .build()
612 .context("Failed to build scan thread pool")?;
613
614 let shutdown_flag = Arc::clone(&ctx.shutdown);
615
616 if self.full_tree && self.full_scan_complete && !self.done.is_empty() {
619 println!("All {} package paths already scanned", self.done.len());
620 return Ok(false);
621 }
622
623 if !self.full_tree {
626 self.incoming.retain(|p| !self.done.contains(p));
627 if self.incoming.is_empty() {
628 if !self.done.is_empty() {
629 println!(
630 "All {} package paths already scanned",
631 self.done.len()
632 );
633 }
634 return Ok(false);
635 }
636 }
637
638 let _scope = SingleSandboxScope::new(self.sandbox.clone())?;
646
647 if self.sandbox.enabled() {
648 if !self.sandbox.run_pre_build(
650 0,
651 &self.config,
652 self.config.script_env(None),
653 )? {
654 warn!("pre-build script failed");
655 }
656 }
657
658 let env = match db.load_pkgsrc_env() {
659 Ok(env) => env,
660 Err(_) => {
661 let env = PkgsrcEnv::fetch(&self.config, &self.sandbox)?;
662 db.store_pkgsrc_env(&env)?;
663 env
664 }
665 };
666 self.pkgsrc_env = Some(env);
667
668 if self.full_tree {
670 self.discover_packages(&pool, &shutdown_flag)?;
671 self.incoming.retain(|p| !self.done.contains(p));
672 }
673
674 if self.incoming.is_empty() {
676 if !self.done.is_empty() {
677 println!(
678 "All {} package paths already scanned",
679 self.done.len()
680 );
681 }
682
683 if self.sandbox.enabled() {
684 self.run_post_build()?;
685 }
686 return Ok(false);
688 }
689
690 db.clear_resolved_depends()?;
692
693 println!("Scanning packages...");
694
695 self.initial_cached = self.done.len();
697
698 let total_count = self.initial_cached + self.incoming.len();
701 let progress = Arc::new(Mutex::new(
702 MultiProgress::new(
703 "Scanning",
704 "",
705 total_count,
706 self.config.scan_threads(),
707 )
708 .expect("Failed to initialize progress display"),
709 ));
710
711 if self.initial_cached > 0 {
713 if let Ok(mut p) = progress.lock() {
714 p.state_mut().cached = self.initial_cached;
715 }
716 }
717
718 let stop_refresh = Arc::new(AtomicBool::new(false));
720
721 let progress_refresh = Arc::clone(&progress);
723 let stop_flag = Arc::clone(&stop_refresh);
724 let shutdown_for_refresh = Arc::clone(&shutdown_flag);
725 let refresh_thread = std::thread::spawn(move || {
726 while !stop_flag.load(Ordering::Relaxed)
727 && !shutdown_for_refresh.load(Ordering::SeqCst)
728 {
729 let has_event = event::poll(REFRESH_INTERVAL).unwrap_or(false);
731
732 if let Ok(mut p) = progress_refresh.lock() {
733 if has_event {
734 let _ = p.handle_event();
735 }
736 let _ = p.render();
737 }
738 }
739 });
740
741 db.begin_transaction()?;
743
744 let mut interrupted = false;
745
746 let config = &self.config;
749 let sandbox = &self.sandbox;
750 let scan_env: Vec<(String, String)> = self
751 .pkgsrc_env
752 .as_ref()
753 .map(|e| {
754 e.cachevars
755 .iter()
756 .map(|(k, v)| (k.clone(), v.clone()))
757 .collect()
758 })
759 .unwrap_or_default();
760
761 loop {
767 if shutdown_flag.load(Ordering::SeqCst) {
771 stop_refresh.store(true, Ordering::Relaxed);
772 if let Ok(mut p) = progress.lock() {
773 let _ = p.finish_interrupted();
774 }
775 interrupted = true;
776 break;
777 }
778
779 let pkgpaths: Vec<PkgPath> = self.incoming.drain().collect();
783 if pkgpaths.is_empty() {
784 break;
785 }
786
787 const CHANNEL_BUFFER_SIZE: usize = 128;
789 let (tx, rx) = std::sync::mpsc::sync_channel::<(
790 PkgPath,
791 Result<Vec<ScanIndex>>,
792 )>(CHANNEL_BUFFER_SIZE);
793
794 let mut new_incoming: HashSet<PkgPath> = HashSet::new();
795
796 std::thread::scope(|s| {
797 let progress_clone = Arc::clone(&progress);
799 let shutdown_clone = Arc::clone(&shutdown_flag);
800 let pool_ref = &pool;
801 let scan_env_ref = &scan_env;
802
803 s.spawn(move || {
804 pool_ref.install(|| {
805 pkgpaths.par_iter().for_each(|pkgpath| {
806 if shutdown_clone.load(Ordering::SeqCst) {
808 return;
809 }
810
811 let pathname =
812 pkgpath.as_path().to_string_lossy().to_string();
813 let thread_id =
814 rayon::current_thread_index().unwrap_or(0);
815
816 if let Ok(mut p) = progress_clone.lock() {
818 p.state_mut()
819 .set_worker_active(thread_id, &pathname);
820 }
821
822 let result = Self::scan_pkgpath_with(
823 config,
824 sandbox,
825 pkgpath,
826 scan_env_ref,
827 &shutdown_clone,
828 );
829
830 if let Ok(mut p) = progress_clone.lock() {
832 p.state_mut().set_worker_idle(thread_id);
833 if result.is_ok() {
834 p.state_mut().increment_completed();
835 } else {
836 p.state_mut().increment_failed();
837 }
838 }
839
840 let _ = tx.send((pkgpath.clone(), result));
842 });
843 });
844 drop(tx);
845 });
846
847 let was_interrupted = shutdown_flag.load(Ordering::SeqCst);
849
850 for (pkgpath, result) in rx {
854 let scanpkgs = match result {
855 Ok(pkgs) => pkgs,
856 Err(e) => {
857 self.scan_failures
858 .push((pkgpath.clone(), e.to_string()));
859 self.done.insert(pkgpath);
860 continue;
861 }
862 };
863 self.done.insert(pkgpath.clone());
864
865 if !scanpkgs.is_empty() {
867 if let Err(e) = db
868 .store_scan_pkgpath(&pkgpath.to_string(), &scanpkgs)
869 {
870 error!(error = %e, "Failed to store scan results");
871 }
872 }
873
874 if self.full_tree || was_interrupted {
877 continue;
878 }
879
880 for pkg in &scanpkgs {
882 if let Some(ref all_deps) = pkg.all_depends {
883 for dep in all_deps {
884 let dep_path = dep.pkgpath();
885 if self.done.contains(dep_path)
886 || new_incoming.contains(dep_path)
887 {
888 continue;
889 }
890 match db
892 .is_pkgpath_scanned(&dep_path.to_string())
893 {
894 Ok(true) => {
895 self.done.insert(dep_path.clone());
896 self.discovered_cached += 1;
897 if let Ok(mut p) = progress.lock() {
898 p.state_mut().total += 1;
899 p.state_mut().cached += 1;
900 }
901 }
902 Ok(false) => {
903 new_incoming.insert(dep_path.clone());
904 if let Ok(mut p) = progress.lock() {
905 p.state_mut().total += 1;
906 }
907 }
908 Err(_) => {}
909 }
910 }
911 }
912 }
913 }
914 });
915
916 new_incoming.retain(|p| !self.done.contains(p));
926 self.incoming = new_incoming;
927 }
928
929 db.commit()?;
931
932 stop_refresh.store(true, Ordering::Relaxed);
934 let _ = refresh_thread.join();
935
936 if !interrupted {
939 let elapsed = if let Ok(mut p) = progress.lock() {
941 p.finish_silent().ok()
942 } else {
943 None
944 };
945
946 let total = self.done.len();
950 let cached = self.initial_cached + self.discovered_cached;
951 let failed = self.scan_failures.len();
952 let succeeded = total.saturating_sub(cached).saturating_sub(failed);
953
954 let elapsed_str =
955 elapsed.map(format_duration).unwrap_or_else(|| "?".to_string());
956
957 if cached > 0 {
958 println!(
959 "Scanned {} package paths in {} ({} scanned, {} cached, {} failed)",
960 total, elapsed_str, succeeded, cached, failed
961 );
962 } else {
963 println!(
964 "Scanned {} package paths in {} ({} succeeded, {} failed)",
965 total, elapsed_str, succeeded, failed
966 );
967 }
968 }
969
970 if self.sandbox.enabled() {
971 self.run_post_build()?;
972 }
973
974 if interrupted {
976 return Ok(true);
977 }
978
979 Ok(false)
980 }
981
982 fn run_post_build(&self) -> anyhow::Result<()> {
984 if !self.sandbox.run_post_build(
985 0,
986 &self.config,
987 self.config.script_env(self.pkgsrc_env.as_ref()),
988 )? {
989 warn!("post-build script failed");
990 }
991 Ok(())
992 }
993
994 pub fn scan_errors(&self) -> impl Iterator<Item = &str> {
996 self.scan_failures.iter().map(|(_, e)| e.as_str())
997 }
998
999 pub fn scan_pkgpath(
1004 &self,
1005 pkgpath: &PkgPath,
1006 ) -> anyhow::Result<Vec<ScanIndex>> {
1007 static NO_SHUTDOWN: AtomicBool = AtomicBool::new(false);
1008 let scan_env: Vec<(String, String)> = self
1009 .pkgsrc_env
1010 .as_ref()
1011 .map(|e| {
1012 e.cachevars
1013 .iter()
1014 .map(|(k, v)| (k.clone(), v.clone()))
1015 .collect()
1016 })
1017 .unwrap_or_default();
1018 Self::scan_pkgpath_with(
1019 &self.config,
1020 &self.sandbox,
1021 pkgpath,
1022 &scan_env,
1023 &NO_SHUTDOWN,
1024 )
1025 }
1026
1027 fn scan_pkgpath_with(
1032 config: &Config,
1033 sandbox: &Sandbox,
1034 pkgpath: &PkgPath,
1035 scan_env: &[(String, String)],
1036 shutdown: &AtomicBool,
1037 ) -> anyhow::Result<Vec<ScanIndex>> {
1038 let pkgpath_str = pkgpath.as_path().display().to_string();
1039 let span = info_span!("scan", pkgpath = %pkgpath_str);
1040 let _guard = span.enter();
1041 debug!("Scanning package");
1042
1043 let pkgsrcdir = config.pkgsrc().display().to_string();
1044 let workdir = format!("{}/{}", pkgsrcdir, pkgpath_str);
1045
1046 trace!(
1047 workdir = %workdir,
1048 scan_env = ?scan_env,
1049 "Executing pkg-scan"
1050 );
1051 let child = sandbox.execute_command(
1052 0,
1053 config.make(),
1054 ["-C", &workdir, "pbulk-index"],
1055 scan_env.to_vec(),
1056 )?;
1057 let output = wait_output_with_shutdown(child, shutdown)?;
1058
1059 if !output.status.success() {
1060 let stderr = String::from_utf8_lossy(&output.stderr);
1061 error!(
1062 exit_code = ?output.status.code(),
1063 stderr = %stderr,
1064 "pkg-scan script failed"
1065 );
1066 let stderr = stderr.trim();
1067 let msg = if stderr.is_empty() {
1068 format!("Scan failed for {}", pkgpath_str)
1069 } else {
1070 format!("Scan failed for {}: {}", pkgpath_str, stderr)
1071 };
1072 bail!(msg);
1073 }
1074
1075 let stdout_str = String::from_utf8_lossy(&output.stdout);
1076 trace!(
1077 stdout_len = stdout_str.len(),
1078 stdout = %stdout_str,
1079 "pkg-scan script output"
1080 );
1081
1082 let reader = BufReader::new(&output.stdout[..]);
1083 let all_results: Vec<ScanIndex> =
1084 ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
1085
1086 let mut seen_pkgnames = HashSet::new();
1092 let mut index: Vec<ScanIndex> = Vec::new();
1093 for pkg in all_results {
1094 if seen_pkgnames.insert(pkg.pkgname.clone()) {
1095 index.push(pkg);
1096 }
1097 }
1098
1099 info!(packages_found = index.len(), "Scan complete");
1100
1101 for pkg in &mut index {
1105 pkg.pkg_location = Some(pkgpath.clone());
1106 debug!(
1107 pkgname = %pkg.pkgname.pkgname(),
1108 skip_reason = ?pkg.pkg_skip_reason,
1109 fail_reason = ?pkg.pkg_fail_reason,
1110 depends_count = pkg.all_depends.as_ref().map_or(0, |v| v.len()),
1111 "Found package in scan"
1112 );
1113 }
1114
1115 Ok(index)
1116 }
1117
1118 pub fn resolve(&mut self, db: &crate::db::Database) -> Result<ScanSummary> {
1124 info!(
1125 done_pkgpaths = self.done.len(),
1126 "Starting dependency resolution"
1127 );
1128
1129 let all_scan_data = db.get_all_scan_indexes()?;
1131
1132 let mut pkgname_to_id: HashMap<PkgName, i64> = HashMap::new();
1134
1135 let mut skip_reasons: HashMap<PkgName, SkipReason> = HashMap::new();
1137 let mut depends: HashMap<PkgName, Vec<PkgName>> = HashMap::new();
1138
1139 for (pkg_id, pkg) in all_scan_data {
1141 if self.packages.contains_key(&pkg.pkgname) {
1143 debug!(pkgname = %pkg.pkgname.pkgname(), "Skipping duplicate PKGNAME");
1144 continue;
1145 }
1146
1147 if let Some(reason) = &pkg.pkg_skip_reason {
1149 if !reason.is_empty() {
1150 info!(pkgname = %pkg.pkgname.pkgname(), reason = %reason, "PKG_SKIP_REASON");
1151 skip_reasons.insert(
1152 pkg.pkgname.clone(),
1153 SkipReason::PkgSkip(reason.clone()),
1154 );
1155 }
1156 }
1157 if let Some(reason) = &pkg.pkg_fail_reason {
1158 if !reason.is_empty()
1159 && !skip_reasons.contains_key(&pkg.pkgname)
1160 {
1161 info!(pkgname = %pkg.pkgname.pkgname(), reason = %reason, "PKG_FAIL_REASON");
1162 skip_reasons.insert(
1163 pkg.pkgname.clone(),
1164 SkipReason::PkgFail(reason.clone()),
1165 );
1166 }
1167 }
1168
1169 pkgname_to_id.insert(pkg.pkgname.clone(), pkg_id);
1170 depends.insert(pkg.pkgname.clone(), Vec::new());
1171 self.packages.insert(pkg.pkgname.clone(), pkg);
1172 }
1173
1174 info!(packages = self.packages.len(), "Loaded packages");
1175
1176 let pkgnames: Vec<PkgName> = self.packages.keys().cloned().collect();
1178
1179 let pkgbase_map: HashMap<&str, Vec<&PkgName>> = {
1181 let mut map: HashMap<&str, Vec<&PkgName>> = HashMap::new();
1182 for pkgname in &pkgnames {
1183 map.entry(pkgname.pkgbase()).or_default().push(pkgname);
1184 }
1185 map
1186 };
1187
1188 let mut match_cache: HashMap<Depend, PkgName> = HashMap::new();
1190
1191 let is_satisfied = |deps: &[PkgName], pattern: &pkgsrc::Pattern| {
1193 deps.iter().any(|existing| pattern.matches(existing.pkgname()))
1194 };
1195
1196 for pkg in self.packages.values_mut() {
1198 let all_deps = match pkg.all_depends.take() {
1199 Some(deps) => deps,
1200 None => continue,
1201 };
1202 let pkg_depends = depends.get_mut(&pkg.pkgname).unwrap();
1203
1204 for depend in all_deps.iter() {
1205 if let Some(pkgname) = match_cache.get(depend) {
1207 if !is_satisfied(pkg_depends, depend.pattern())
1208 && !pkg_depends.contains(pkgname)
1209 {
1210 pkg_depends.push(pkgname.clone());
1211 }
1212 continue;
1213 }
1214
1215 let candidates: Vec<&PkgName> = if let Some(base) =
1217 depend.pattern().pkgbase()
1218 {
1219 pkgbase_map.get(base).map_or(Vec::new(), |v| {
1220 v.iter()
1221 .filter(|c| depend.pattern().matches(c.pkgname()))
1222 .copied()
1223 .collect()
1224 })
1225 } else {
1226 pkgnames
1227 .iter()
1228 .filter(|c| depend.pattern().matches(c.pkgname()))
1229 .collect()
1230 };
1231
1232 let mut best: Option<&PkgName> = None;
1234 let mut match_error: Option<pkgsrc::PatternError> = None;
1235 for candidate in candidates {
1236 best = match best {
1237 None => Some(candidate),
1238 Some(current) => {
1239 match depend.pattern().best_match_pbulk(
1240 current.pkgname(),
1241 candidate.pkgname(),
1242 ) {
1243 Ok(Some(m)) if m == candidate.pkgname() => {
1244 Some(candidate)
1245 }
1246 Ok(_) => Some(current),
1247 Err(e) => {
1248 match_error = Some(e);
1249 break;
1250 }
1251 }
1252 }
1253 };
1254 }
1255
1256 if let Some(e) = match_error {
1257 let reason = format!(
1258 "{}: pattern error for {}: {}",
1259 pkg.pkgname.pkgname(),
1260 depend.pattern().pattern(),
1261 e
1262 );
1263 if !skip_reasons.contains_key(&pkg.pkgname) {
1264 skip_reasons.insert(
1265 pkg.pkgname.clone(),
1266 SkipReason::PkgFail(reason),
1267 );
1268 }
1269 continue;
1270 }
1271
1272 if let Some(pkgname) = best {
1273 if !is_satisfied(pkg_depends, depend.pattern())
1274 && !pkg_depends.contains(pkgname)
1275 {
1276 pkg_depends.push(pkgname.clone());
1277 }
1278 match_cache.insert(depend.clone(), pkgname.clone());
1279 } else {
1280 let pattern = depend.pattern().pattern();
1283 let fail_reason = format!(
1285 "\"could not resolve dependency \"{}\"\"",
1286 pattern
1287 );
1288 pkg.pkg_fail_reason = Some(fail_reason);
1289 let msg = format!(
1290 "No match found for dependency {} of package {}",
1291 pattern,
1292 pkg.pkgname.pkgname()
1293 );
1294 match skip_reasons.get_mut(&pkg.pkgname) {
1295 Some(SkipReason::UnresolvedDep(existing)) => {
1296 existing.push('\n');
1297 existing.push_str(&msg);
1298 }
1299 None => {
1300 skip_reasons.insert(
1301 pkg.pkgname.clone(),
1302 SkipReason::UnresolvedDep(msg),
1303 );
1304 }
1305 _ => {}
1306 }
1307 }
1308 }
1309 pkg.all_depends = Some(all_deps);
1310 }
1311
1312 loop {
1314 let mut new_skip_reasons: Vec<(PkgName, SkipReason)> = Vec::new();
1315 for (pkgname, pkg_depends) in &depends {
1316 if skip_reasons.contains_key(pkgname) {
1317 continue;
1318 }
1319 for dep in pkg_depends {
1320 if let Some(dep_reason) = skip_reasons.get(dep) {
1321 let reason = match dep_reason {
1323 SkipReason::PkgSkip(_)
1324 | SkipReason::IndirectSkip(_) => {
1325 SkipReason::IndirectSkip(format!(
1326 "dependency {} skipped",
1327 dep.pkgname()
1328 ))
1329 }
1330 _ => SkipReason::IndirectFail(format!(
1331 "dependency {} failed",
1332 dep.pkgname()
1333 )),
1334 };
1335 new_skip_reasons.push((pkgname.clone(), reason));
1336 break;
1337 }
1338 }
1339 }
1340 if new_skip_reasons.is_empty() {
1341 break;
1342 }
1343 for (pkgname, reason) in new_skip_reasons {
1344 skip_reasons.insert(pkgname, reason);
1345 }
1346 }
1347
1348 let mut packages: Vec<ScanResult> = Vec::new();
1350 let mut count_buildable = 0;
1351
1352 for (pkgname, index) in std::mem::take(&mut self.packages) {
1353 let Some(pkgpath) = index.pkg_location.clone() else {
1354 error!(pkgname = %pkgname, "Package missing PKG_LOCATION, skipping");
1355 continue;
1356 };
1357 let resolved_depends = depends.remove(&pkgname).unwrap_or_default();
1358 let result = match skip_reasons.remove(&pkgname) {
1359 Some(reason) => ScanResult::Skipped {
1360 pkgpath,
1361 reason,
1362 index: Some(index),
1363 resolved_depends,
1364 },
1365 None => {
1366 count_buildable += 1;
1367 ScanResult::Buildable(ResolvedPackage {
1368 index,
1369 pkgpath,
1370 resolved_depends,
1371 })
1372 }
1373 };
1374 packages.push(result);
1375 }
1376
1377 for (pkgpath, error) in &self.scan_failures {
1379 packages.push(ScanResult::ScanFail {
1380 pkgpath: pkgpath.clone(),
1381 error: error.clone(),
1382 });
1383 }
1384
1385 debug!(count_buildable, "Checking for circular dependencies");
1387 let mut graph = DiGraphMap::new();
1388 for pkg in &packages {
1389 if let ScanResult::Buildable(resolved) = pkg {
1390 for dep in &resolved.resolved_depends {
1391 graph.add_edge(
1392 dep.pkgname(),
1393 resolved.pkgname().pkgname(),
1394 (),
1395 );
1396 }
1397 }
1398 }
1399 if let Some(cycle) = find_cycle(&graph) {
1400 let mut err = "Circular dependencies detected:\n".to_string();
1401 for n in cycle.iter().rev() {
1402 err.push_str(&format!("\t{}\n", n));
1403 }
1404 err.push_str(&format!("\t{}", cycle.last().unwrap()));
1405 error!(cycle = ?cycle, "Circular dependency detected");
1406 bail!(err);
1407 }
1408
1409 info!(
1410 count_buildable,
1411 count_preskip = packages
1412 .iter()
1413 .filter(|p| matches!(
1414 p,
1415 ScanResult::Skipped { reason: SkipReason::PkgSkip(_), .. }
1416 ))
1417 .count(),
1418 count_prefail = packages
1419 .iter()
1420 .filter(|p| matches!(
1421 p,
1422 ScanResult::Skipped { reason: SkipReason::PkgFail(_), .. }
1423 ))
1424 .count(),
1425 count_unresolved = packages
1426 .iter()
1427 .filter(|p| matches!(
1428 p,
1429 ScanResult::Skipped {
1430 reason: SkipReason::UnresolvedDep(_),
1431 ..
1432 }
1433 ))
1434 .count(),
1435 "Resolution complete"
1436 );
1437
1438 let mut resolved_deps: Vec<(i64, i64)> = Vec::new();
1440 for pkg in &packages {
1441 if let ScanResult::Buildable(resolved) = pkg {
1442 if let Some(&pkg_id) = pkgname_to_id.get(resolved.pkgname()) {
1443 for dep in &resolved.resolved_depends {
1444 if let Some(&dep_id) = pkgname_to_id.get(dep) {
1445 resolved_deps.push((pkg_id, dep_id));
1446 }
1447 }
1448 }
1449 }
1450 }
1451 if !resolved_deps.is_empty() {
1452 db.store_resolved_dependencies_batch(&resolved_deps)?;
1453 debug!(count = resolved_deps.len(), "Stored resolved dependencies");
1454 }
1455
1456 Ok(ScanSummary { pkgpaths: self.done.len(), packages })
1457 }
1458}
1459
1460pub fn find_cycle<'a>(
1461 graph: &'a DiGraphMap<&'a str, ()>,
1462) -> Option<Vec<&'a str>> {
1463 let mut visited = HashSet::new();
1464 let mut in_stack = HashSet::new();
1465 let mut stack = Vec::new();
1466
1467 for node in graph.nodes() {
1468 if visited.contains(&node) {
1469 continue;
1470 }
1471 if let Some(cycle) =
1472 dfs(graph, node, &mut visited, &mut stack, &mut in_stack)
1473 {
1474 return Some(cycle);
1475 }
1476 }
1477 None
1478}
1479
1480fn dfs<'a>(
1481 graph: &'a DiGraphMap<&'a str, ()>,
1482 node: &'a str,
1483 visited: &mut HashSet<&'a str>,
1484 stack: &mut Vec<&'a str>,
1485 in_stack: &mut HashSet<&'a str>,
1486) -> Option<Vec<&'a str>> {
1487 visited.insert(node);
1488 stack.push(node);
1489 in_stack.insert(node);
1490 for neighbor in graph.neighbors(node) {
1491 if in_stack.contains(neighbor) {
1492 if let Some(pos) = stack.iter().position(|&n| n == neighbor) {
1493 return Some(stack[pos..].to_vec());
1494 }
1495 } else if !visited.contains(neighbor) {
1496 let cycle = dfs(graph, neighbor, visited, stack, in_stack);
1497 if cycle.is_some() {
1498 return cycle;
1499 }
1500 }
1501 }
1502 stack.pop();
1503 in_stack.remove(node);
1504 None
1505}