1use crate::tui::MultiProgress;
61use crate::{Config, Sandbox};
62use anyhow::{Context, Result, bail};
63use petgraph::graphmap::DiGraphMap;
64use pkgsrc::{Depend, PkgName, PkgPath, ScanIndex};
65use rayon::prelude::*;
66use std::collections::{HashMap, HashSet};
67use std::io::BufReader;
68use std::sync::atomic::{AtomicBool, Ordering};
69use std::sync::{Arc, Mutex};
70use std::time::Duration;
71use tracing::{debug, error, info, trace};
72
73#[derive(Clone, Debug)]
77pub enum SkipReason {
78 PkgSkipReason(String),
83 PkgFailReason(String),
88}
89
90#[derive(Clone, Debug)]
92pub struct SkippedPackage {
93 pub pkgname: PkgName,
95 pub pkgpath: Option<PkgPath>,
97 pub reason: SkipReason,
99}
100
101#[derive(Clone, Debug, Default)]
106pub struct ScanResult {
107 pub buildable: HashMap<PkgName, ScanIndex>,
111 pub skipped: Vec<SkippedPackage>,
113}
114
115#[derive(Debug, Default)]
145pub struct Scan {
146 config: Config,
147 sandbox: Sandbox,
148 incoming: HashSet<PkgPath>,
149 done: HashMap<PkgPath, Vec<ScanIndex>>,
150 resolved: HashMap<PkgName, ScanIndex>,
151}
152
153impl Scan {
154 pub fn new(config: &Config) -> Scan {
155 let sandbox = Sandbox::new(config);
156 debug!(pkgsrc = %config.pkgsrc().display(),
157 make = %config.make().display(),
158 scan_threads = config.scan_threads(),
159 "Created new Scan instance"
160 );
161 Scan { config: config.clone(), sandbox, ..Default::default() }
162 }
163
164 pub fn add(&mut self, pkgpath: &PkgPath) {
165 info!(pkgpath = %pkgpath.as_path().display(), "Adding package to scan queue");
166 self.incoming.insert(pkgpath.clone());
167 }
168
169 pub fn start(&mut self) -> anyhow::Result<()> {
170 info!(
171 incoming_count = self.incoming.len(),
172 sandbox_enabled = self.sandbox.enabled(),
173 "Starting package scan"
174 );
175
176 let pool = rayon::ThreadPoolBuilder::new()
177 .num_threads(self.config.scan_threads())
178 .build()
179 .context("Failed to build scan thread pool")?;
180
181 let script_envs = self.config.script_env();
186
187 if self.sandbox.enabled() {
188 println!("Creating sandbox...");
189 if let Err(e) = self.sandbox.create(0) {
190 if let Err(destroy_err) = self.sandbox.destroy(0) {
191 eprintln!(
192 "Warning: failed to destroy sandbox: {}",
193 destroy_err
194 );
195 }
196 return Err(e);
197 }
198
199 if let Some(pre_build) = self.config.script("pre-build") {
201 debug!("Running pre-build script");
202 let child = self.sandbox.execute(
203 0,
204 pre_build,
205 script_envs.clone(),
206 None,
207 None,
208 )?;
209 let output = child
210 .wait_with_output()
211 .context("Failed to wait for pre-build")?;
212 if !output.status.success() {
213 let stderr = String::from_utf8_lossy(&output.stderr);
214 error!(exit_code = ?output.status.code(), stderr = %stderr, "pre-build script failed");
215 }
216 }
217 }
218
219 println!("Scanning packages...");
220
221 let progress = Arc::new(Mutex::new(
223 MultiProgress::new(
224 "Scanning",
225 "Scanned",
226 self.incoming.len(),
227 self.config.scan_threads(),
228 false,
229 )
230 .expect("Failed to initialize progress display"),
231 ));
232
233 let stop_refresh = Arc::new(AtomicBool::new(false));
235
236 let progress_refresh = Arc::clone(&progress);
238 let stop_flag = Arc::clone(&stop_refresh);
239 let refresh_thread = std::thread::spawn(move || {
240 while !stop_flag.load(Ordering::Relaxed) {
241 if let Ok(mut p) = progress_refresh.lock() {
242 let _ = p.render();
243 }
244 std::thread::sleep(Duration::from_millis(100));
245 }
246 });
247
248 let mut scan_errors: Vec<String> = Vec::new();
254 loop {
255 let mut parpaths: Vec<(PkgPath, Result<Vec<ScanIndex>>)> = vec![];
259 for pkgpath in &self.incoming {
260 parpaths.push((pkgpath.clone(), Ok(vec![])));
261 }
262
263 let progress_clone = Arc::clone(&progress);
264 pool.install(|| {
265 parpaths.par_iter_mut().for_each(|pkg| {
266 let (pkgpath, result) = pkg;
267 let pathname =
268 pkgpath.as_path().to_string_lossy().to_string();
269
270 let thread_id = rayon::current_thread_index().unwrap_or(0);
272
273 if let Ok(mut p) = progress_clone.lock() {
275 p.state_mut().set_worker_active(thread_id, &pathname);
276 }
277
278 *result = self.scan_pkgpath(pkgpath);
279
280 if let Ok(mut p) = progress_clone.lock() {
282 p.state_mut().set_worker_idle(thread_id);
283 }
284 });
285 });
286
287 let mut new_incoming: HashSet<PkgPath> = HashSet::new();
293 for (pkgpath, scanpkgs) in parpaths.drain(..) {
294 let scanpkgs = match scanpkgs {
295 Ok(pkgs) => {
296 if let Ok(mut p) = progress.lock() {
297 p.state_mut().increment_completed();
298 }
299 pkgs
300 }
301 Err(e) => {
302 scan_errors.push(format!("{}", e));
303 if let Ok(mut p) = progress.lock() {
304 p.state_mut().increment_failed();
305 }
306 self.done.insert(pkgpath.clone(), vec![]);
307 continue;
308 }
309 };
310 self.done.insert(pkgpath.clone(), scanpkgs.clone());
311 for pkg in scanpkgs {
312 for dep in pkg.all_depends {
313 if !self.done.contains_key(dep.pkgpath())
314 && !self.incoming.contains(dep.pkgpath())
315 && new_incoming.insert(dep.pkgpath().clone())
316 {
317 if let Ok(mut p) = progress.lock() {
319 p.state_mut().total += 1;
320 }
321 }
322 }
323 }
324 }
325
326 self.incoming = new_incoming;
332 if self.incoming.is_empty() {
333 break;
334 }
335 }
336
337 if self.sandbox.enabled() {
338 if let Some(post_build) = self.config.script("post-build") {
340 debug!("Running post-build script");
341 let child = self.sandbox.execute(
342 0,
343 post_build,
344 script_envs,
345 None,
346 None,
347 )?;
348 let output = child
349 .wait_with_output()
350 .context("Failed to wait for post-build")?;
351 if !output.status.success() {
352 let stderr = String::from_utf8_lossy(&output.stderr);
353 error!(exit_code = ?output.status.code(), stderr = %stderr, "post-build script failed");
354 }
355 }
356
357 self.sandbox.destroy(0)?;
358 }
359
360 stop_refresh.store(true, Ordering::Relaxed);
362 let _ = refresh_thread.join();
363
364 if let Ok(mut p) = progress.lock() {
365 let _ = p.finish();
366 }
367
368 if !scan_errors.is_empty() {
369 for err in &scan_errors {
370 eprintln!("{}", err);
371 }
372 bail!("{} package(s) failed to scan", scan_errors.len());
373 }
374
375 Ok(())
376 }
377
378 pub fn scan_pkgpath(
383 &self,
384 pkgpath: &PkgPath,
385 ) -> anyhow::Result<Vec<ScanIndex>> {
386 let pkgpath_str = pkgpath.as_path().display().to_string();
387 debug!(pkgpath = %pkgpath_str, "Scanning package");
388
389 let bmake = self.config.make().display().to_string();
390 let pkgsrcdir = self.config.pkgsrc().display().to_string();
391 let script = format!(
392 "cd {}/{} && {} pbulk-index\n",
393 pkgsrcdir, pkgpath_str, bmake
394 );
395
396 trace!(pkgpath = %pkgpath_str,
397 script = %script,
398 "Executing pkg-scan"
399 );
400 let child = self.sandbox.execute_script(0, &script, vec![])?;
401 let output = child.wait_with_output()?;
402
403 if !output.status.success() {
404 let stderr = String::from_utf8_lossy(&output.stderr);
405 error!(pkgpath = %pkgpath_str,
406 exit_code = ?output.status.code(),
407 stderr = %stderr,
408 "pkg-scan script failed"
409 );
410 let stderr = stderr.trim();
411 let msg = if stderr.is_empty() {
412 format!("Scan failed for {}", pkgpath_str)
413 } else {
414 format!("Scan failed for {}: {}", pkgpath_str, stderr)
415 };
416 bail!(msg);
417 }
418
419 let stdout_str = String::from_utf8_lossy(&output.stdout);
420 trace!(pkgpath = %pkgpath_str,
421 stdout_len = stdout_str.len(),
422 stdout = %stdout_str,
423 "pkg-scan script output"
424 );
425
426 let reader = BufReader::new(&output.stdout[..]);
427 let mut index = ScanIndex::from_reader(reader)?;
428
429 info!(pkgpath = %pkgpath_str,
430 packages_found = index.len(),
431 "Scan complete for pkgpath"
432 );
433
434 for pkg in &mut index {
438 pkg.pkg_location = Some(pkgpath.clone());
439 debug!(pkgpath = %pkgpath_str,
440 pkgname = %pkg.pkgname.pkgname(),
441 skip_reason = ?pkg.pkg_skip_reason,
442 fail_reason = ?pkg.pkg_fail_reason,
443 depends_count = pkg.all_depends.len(),
444 "Found package in scan"
445 );
446 }
447
448 Ok(index)
449 }
450
451 pub fn scanned(&self) -> impl Iterator<Item = &ScanIndex> {
453 self.done.values().flatten()
454 }
455
456 pub fn write_log(&self, path: &std::path::Path) -> anyhow::Result<()> {
458 let mut out = String::new();
459 for idx in self.scanned() {
460 out.push_str(&format!("PKGNAME={}\n", idx.pkgname.pkgname()));
461 if let Some(ref loc) = idx.pkg_location {
462 out.push_str(&format!(
463 "PKG_LOCATION={}\n",
464 loc.as_path().display()
465 ));
466 }
467 if !idx.all_depends.is_empty() {
468 let deps: Vec<String> = idx
469 .all_depends
470 .iter()
471 .map(|d| d.pkgpath().as_path().display().to_string())
472 .collect();
473 out.push_str(&format!("ALL_DEPENDS={}\n", deps.join(" ")));
474 }
475 if !idx.depends.is_empty() {
476 let deps: Vec<&str> =
477 idx.depends.iter().map(|d| d.pkgname()).collect();
478 out.push_str(&format!("DEPENDS={}\n", deps.join(" ")));
479 }
480 if !idx.multi_version.is_empty() {
481 out.push_str(&format!(
482 "MULTI_VERSION={}\n",
483 idx.multi_version.join(" ")
484 ));
485 }
486 if let Some(ref v) = idx.pkg_skip_reason {
487 out.push_str(&format!("PKG_SKIP_REASON={}\n", v));
488 }
489 if let Some(ref v) = idx.pkg_fail_reason {
490 out.push_str(&format!("PKG_FAIL_REASON={}\n", v));
491 }
492 if let Some(ref v) = idx.categories {
493 out.push_str(&format!("CATEGORIES={}\n", v));
494 }
495 if let Some(ref v) = idx.maintainer {
496 out.push_str(&format!("MAINTAINER={}\n", v));
497 }
498 if let Some(ref v) = idx.bootstrap_pkg {
499 out.push_str(&format!("BOOTSTRAP_PKG={}\n", v));
500 }
501 if let Some(ref v) = idx.usergroup_phase {
502 out.push_str(&format!("USERGROUP_PHASE={}\n", v));
503 }
504 if let Some(ref v) = idx.use_destdir {
505 out.push_str(&format!("USE_DESTDIR={}\n", v));
506 }
507 if let Some(ref v) = idx.no_bin_on_ftp {
508 out.push_str(&format!("NO_BIN_ON_FTP={}\n", v));
509 }
510 if let Some(ref v) = idx.restricted {
511 out.push_str(&format!("RESTRICTED={}\n", v));
512 }
513 if let Some(ref v) = idx.pbulk_weight {
514 out.push_str(&format!("PBULK_WEIGHT={}\n", v));
515 }
516 out.push('\n');
517 }
518 std::fs::write(path, &out)?;
519 Ok(())
520 }
521
522 pub fn resolve(&mut self) -> Result<ScanResult> {
531 info!(
532 done_pkgpaths = self.done.len(),
533 "Starting dependency resolution"
534 );
535
536 let mut pkgnames: HashSet<PkgName> = HashSet::new();
549 let mut skipped: Vec<SkippedPackage> = Vec::new();
550
551 for (pkgpath, index) in &self.done {
553 debug!(pkgpath = %pkgpath.as_path().display(),
554 packages_in_index = index.len(),
555 "Processing done entry"
556 );
557 }
558
559 for index in self.done.values() {
560 for pkg in index {
561 if let Some(reason) = &pkg.pkg_skip_reason {
563 if !reason.is_empty() {
564 info!(pkgname = %pkg.pkgname.pkgname(),
565 reason = %reason,
566 "Skipping package due to PKG_SKIP_REASON"
567 );
568 skipped.push(SkippedPackage {
569 pkgname: pkg.pkgname.clone(),
570 pkgpath: pkg.pkg_location.clone(),
571 reason: SkipReason::PkgSkipReason(reason.clone()),
572 });
573 continue;
574 }
575 }
576 if let Some(reason) = &pkg.pkg_fail_reason {
577 if !reason.is_empty() {
578 info!(pkgname = %pkg.pkgname.pkgname(),
579 reason = %reason,
580 "Skipping package due to PKG_FAIL_REASON"
581 );
582 skipped.push(SkippedPackage {
583 pkgname: pkg.pkgname.clone(),
584 pkgpath: pkg.pkg_location.clone(),
585 reason: SkipReason::PkgFailReason(reason.clone()),
586 });
587 continue;
588 }
589 }
590
591 debug!(pkgname = %pkg.pkgname.pkgname(),
592 "Adding package to resolved set"
593 );
594 pkgnames.insert(pkg.pkgname.clone());
595 self.resolved.insert(pkg.pkgname.clone(), pkg.clone());
596 }
597 }
598
599 info!(
600 resolved_count = self.resolved.len(),
601 skipped_count = skipped.len(),
602 "Initial resolution complete"
603 );
604
605 if !skipped.is_empty() {
606 println!(
607 "Skipping {} packages with PKG_SKIP_REASON or PKG_FAIL_REASON",
608 skipped.len()
609 );
610 }
611
612 let mut match_cache: HashMap<Depend, PkgName> = HashMap::new();
617 let mut errors: Vec<String> = Vec::new();
618
619 for pkg in self.resolved.values_mut() {
620 for depend in &pkg.all_depends {
621 if let Some(pkgname) = match_cache.get(depend) {
625 pkg.depends.push(pkgname.clone().clone());
626 continue;
627 }
628 let mut best: Option<&PkgName> = None;
632 for candidate in &pkgnames {
633 if depend.pattern().matches(candidate.pkgname()) {
634 if let Some(current) = best {
635 best = match depend.pattern().best_match(
636 current.pkgname(),
637 candidate.pkgname(),
638 ) {
639 Some(m) if m == current.pkgname() => {
640 Some(current)
641 }
642 Some(m) if m == candidate.pkgname() => {
643 Some(candidate)
644 }
645 Some(_) => todo!(),
646 None => None,
647 };
648 } else {
649 best = Some(candidate);
650 }
651 }
652 }
653 if let Some(pkgname) = best {
658 pkg.depends.push(pkgname.clone());
659 match_cache.insert(depend.clone(), pkgname.clone());
660 } else {
661 errors.push(format!(
662 "No match found for {} in {}",
663 depend.pattern().pattern(),
664 pkg.pkgname.pkgname()
665 ));
666 }
667 }
668 }
669
670 if !errors.is_empty() {
671 for err in &errors {
672 error!(error = %err, "Unresolved dependency");
673 }
674 bail!("Unresolved dependencies:\n {}", errors.join("\n "));
675 }
676
677 debug!(
681 resolved_count = self.resolved.len(),
682 "Checking for circular dependencies"
683 );
684 let mut graph = DiGraphMap::new();
685 for (pkgname, index) in &self.resolved {
686 for dep in &index.depends {
687 graph.add_edge(dep.pkgname(), pkgname.pkgname(), ());
688 }
689 }
690 if let Some(cycle) = find_cycle(&graph) {
691 let mut err = "Circular dependencies detected:\n".to_string();
692 for n in cycle.iter().rev() {
693 err.push_str(&format!("\t{}\n", n));
694 }
695 err.push_str(&format!("\t{}", cycle.last().unwrap()));
696 error!(cycle = ?cycle, "Circular dependency detected");
697 bail!(err);
698 }
699
700 info!(
701 buildable_count = self.resolved.len(),
702 skipped_count = skipped.len(),
703 "Resolution complete"
704 );
705
706 for pkgname in self.resolved.keys() {
708 debug!(pkgname = %pkgname.pkgname(), "Package is buildable");
709 }
710
711 Ok(ScanResult { buildable: self.resolved.clone(), skipped })
712 }
713}
714
715pub fn find_cycle<'a>(
716 graph: &'a DiGraphMap<&'a str, ()>,
717) -> Option<Vec<&'a str>> {
718 let mut visited = HashSet::new();
719 let mut in_stack = HashSet::new();
720 let mut stack = Vec::new();
721
722 for node in graph.nodes() {
723 if visited.contains(&node) {
724 continue;
725 }
726 let cycle = dfs(graph, node, &mut visited, &mut stack, &mut in_stack);
727 if cycle.is_some() {
728 return cycle;
729 }
730 }
731 None
732}
733
734fn dfs<'a>(
735 graph: &'a DiGraphMap<&'a str, ()>,
736 node: &'a str,
737 visited: &mut HashSet<&'a str>,
738 stack: &mut Vec<&'a str>,
739 in_stack: &mut HashSet<&'a str>,
740) -> Option<Vec<&'a str>> {
741 visited.insert(node);
742 stack.push(node);
743 in_stack.insert(node);
744 for neighbor in graph.neighbors(node) {
745 if in_stack.contains(neighbor) {
746 if let Some(pos) = stack.iter().position(|&n| n == neighbor) {
747 return Some(stack[pos..].to_vec());
748 }
749 } else if !visited.contains(neighbor) {
750 let cycle = dfs(graph, neighbor, visited, stack, in_stack);
751 if cycle.is_some() {
752 return cycle;
753 }
754 }
755 }
756 stack.pop();
757 in_stack.remove(node);
758 None
759}