1use crate::scan::ResolvedIndex;
69use crate::scan::ScanFailure;
70use crate::tui::{MultiProgress, format_duration};
71use crate::{Config, RunContext, Sandbox};
72use anyhow::{Context, bail};
73use glob::Pattern;
74use indexmap::IndexMap;
75use pkgsrc::{PkgName, PkgPath};
76use std::collections::{HashMap, HashSet, VecDeque};
77use std::fs::{self, File, OpenOptions};
78use std::path::{Path, PathBuf};
79use std::process::{Command, ExitStatus, Stdio};
80use std::sync::atomic::{AtomicBool, Ordering};
81use std::sync::{Arc, Mutex, mpsc, mpsc::Sender};
82use std::time::{Duration, Instant};
83use tracing::{debug, error, info, trace, warn};
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87enum Stage {
88 PreClean,
89 Depends,
90 Checksum,
91 Configure,
92 Build,
93 Install,
94 Package,
95 Deinstall,
96 Clean,
97}
98
99impl Stage {
100 fn as_str(&self) -> &'static str {
101 match self {
102 Stage::PreClean => "pre-clean",
103 Stage::Depends => "depends",
104 Stage::Checksum => "checksum",
105 Stage::Configure => "configure",
106 Stage::Build => "build",
107 Stage::Install => "install",
108 Stage::Package => "package",
109 Stage::Deinstall => "deinstall",
110 Stage::Clean => "clean",
111 }
112 }
113}
114
115#[derive(Debug)]
117enum PkgBuildResult {
118 Success,
119 Failed,
120 Skipped,
121}
122
123#[derive(Debug, Clone, Copy)]
125enum RunAs {
126 Root,
127 User,
128}
129
130trait BuildCallback: Send {
132 fn stage(&mut self, stage: &str);
133}
134
135struct PkgBuilder<'a> {
137 config: &'a Config,
138 sandbox: &'a Sandbox,
139 sandbox_id: usize,
140 pkginfo: &'a ResolvedIndex,
141 logdir: PathBuf,
142 build_user: Option<String>,
143 envs: Vec<(String, String)>,
144 output_tx: Option<Sender<ChannelCommand>>,
145 options: &'a BuildOptions,
146}
147
148impl<'a> PkgBuilder<'a> {
149 fn new(
150 config: &'a Config,
151 sandbox: &'a Sandbox,
152 sandbox_id: usize,
153 pkginfo: &'a ResolvedIndex,
154 envs: Vec<(String, String)>,
155 output_tx: Option<Sender<ChannelCommand>>,
156 options: &'a BuildOptions,
157 ) -> Self {
158 let logdir = config.logdir().join(pkginfo.pkgname.pkgname());
159 let build_user = config.build_user().map(|s| s.to_string());
160 Self {
161 config,
162 sandbox,
163 sandbox_id,
164 pkginfo,
165 logdir,
166 build_user,
167 envs,
168 output_tx,
169 options,
170 }
171 }
172
173 fn run_cmd(&self, cmd: &Path, args: &[&str]) -> Option<String> {
175 let mut command = self.sandbox.command(self.sandbox_id, cmd);
176 command.args(args);
177 self.apply_envs(&mut command, &[]);
178 match command.output() {
179 Ok(output) if output.status.success() => {
180 Some(String::from_utf8_lossy(&output.stdout).into_owned())
181 }
182 Ok(output) => {
183 let stderr = String::from_utf8_lossy(&output.stderr);
184 debug!(
185 cmd = %cmd.display(),
186 exit_code = ?output.status.code(),
187 stderr = %stderr.trim(),
188 "command failed"
189 );
190 None
191 }
192 Err(e) => {
193 debug!(cmd = %cmd.display(), error = %e, "command execution error");
194 None
195 }
196 }
197 }
198
199 fn check_up_to_date(&self) -> anyhow::Result<bool> {
201 let packages =
202 self.config.packages().context("pkgsrc.packages not configured")?;
203 let pkgtools =
204 self.config.pkgtools().context("pkgsrc.pkgtools not configured")?;
205
206 let pkgname = self.pkginfo.pkgname.pkgname();
207 let pkgfile = packages.join("All").join(format!("{}.tgz", pkgname));
208
209 if !pkgfile.exists() {
211 debug!(pkgname, path = %pkgfile.display(), "package file not found");
212 return Ok(false);
213 }
214
215 let pkgfile_str = pkgfile.to_string_lossy();
216 let pkg_info = pkgtools.join("pkg_info");
217 let pkg_admin = pkgtools.join("pkg_admin");
218
219 let Some(build_info) = self.run_cmd(&pkg_info, &["-qb", &pkgfile_str])
221 else {
222 debug!(pkgname, "pkg_info -qb failed or returned empty");
223 return Ok(false);
224 };
225 debug!(
226 pkgname,
227 lines = build_info.lines().count(),
228 "checking BUILD_INFO"
229 );
230
231 for line in build_info.lines() {
232 let Some((file, file_id)) = line.split_once(':') else {
233 continue;
234 };
235 let file_id = file_id.trim();
236 if file.is_empty() || file_id.is_empty() {
237 continue;
238 }
239
240 let src_file = self.config.pkgsrc().join(file);
241 if !src_file.exists() {
242 debug!(pkgname, file, "source file missing");
243 return Ok(false);
244 }
245
246 if file_id.starts_with("$NetBSD") {
247 let Ok(content) = std::fs::read_to_string(&src_file) else {
249 return Ok(false);
250 };
251 let id = content.lines().find_map(|line| {
252 if let Some(start) = line.find("$NetBSD") {
253 if let Some(end) = line[start + 1..].find('$') {
254 return Some(&line[start..start + 1 + end + 1]);
255 }
256 }
257 None
258 });
259 if id != Some(file_id) {
260 debug!(pkgname, file, "CVS ID mismatch");
261 return Ok(false);
262 }
263 } else {
264 let src_file_str = src_file.to_string_lossy();
266 let Some(hash) =
267 self.run_cmd(&pkg_admin, &["digest", &src_file_str])
268 else {
269 debug!(pkgname, file, "pkg_admin digest failed");
270 return Ok(false);
271 };
272 let hash = hash.trim();
273 if hash != file_id {
274 debug!(
275 pkgname,
276 file,
277 path = %src_file.display(),
278 expected = file_id,
279 actual = hash,
280 "hash mismatch"
281 );
282 return Ok(false);
283 }
284 }
285 }
286
287 let Some(pkg_deps) = self.run_cmd(&pkg_info, &["-qN", &pkgfile_str])
289 else {
290 return Ok(false);
291 };
292
293 let recorded_deps: HashSet<&str> = pkg_deps
295 .lines()
296 .map(|l| l.trim())
297 .filter(|l| !l.is_empty())
298 .collect();
299 let expected_deps: HashSet<&str> =
300 self.pkginfo.depends.iter().map(|d| d.pkgname()).collect();
301
302 if recorded_deps != expected_deps {
304 debug!(
305 pkgname,
306 recorded = recorded_deps.len(),
307 expected = expected_deps.len(),
308 "dependency list changed"
309 );
310 return Ok(false);
311 }
312
313 let pkgfile_mtime = match pkgfile.metadata().and_then(|m| m.modified())
314 {
315 Ok(t) => t,
316 Err(_) => return Ok(false),
317 };
318
319 for dep in &recorded_deps {
321 let dep_pkg = packages.join("All").join(format!("{}.tgz", dep));
322 if !dep_pkg.exists() {
323 debug!(pkgname, dep, "dependency package missing");
324 return Ok(false);
325 }
326
327 let dep_mtime = match dep_pkg.metadata().and_then(|m| m.modified())
328 {
329 Ok(t) => t,
330 Err(_) => return Ok(false),
331 };
332
333 if dep_mtime > pkgfile_mtime {
334 debug!(pkgname, dep, "dependency is newer");
335 return Ok(false);
336 }
337 }
338
339 debug!(pkgname, "package is up-to-date");
340 Ok(true)
341 }
342
343 fn build<C: BuildCallback>(
345 &self,
346 callback: &mut C,
347 ) -> anyhow::Result<PkgBuildResult> {
348 let pkgname = self.pkginfo.pkgname.pkgname();
349 let Some(pkgpath) = &self.pkginfo.pkg_location else {
350 bail!("Could not get PKGPATH for {}", pkgname);
351 };
352
353 if !self.options.force_rebuild && self.check_up_to_date()? {
355 return Ok(PkgBuildResult::Skipped);
356 }
357
358 if self.logdir.exists() {
360 fs::remove_dir_all(&self.logdir)?;
361 }
362 fs::create_dir_all(&self.logdir)?;
363
364 let work_log = self.logdir.join("work.log");
366 File::create(&work_log)?;
367 if let Some(ref user) = self.build_user {
368 let bob_log = File::options()
369 .create(true)
370 .append(true)
371 .open(self.logdir.join("bob.log"))?;
372 let bob_log_err = bob_log.try_clone()?;
373 let _ = Command::new("chown")
374 .arg(user)
375 .arg(&work_log)
376 .stdout(bob_log)
377 .stderr(bob_log_err)
378 .status();
379 }
380
381 let pkgdir = self.config.pkgsrc().join(pkgpath.as_path());
382
383 callback.stage(Stage::PreClean.as_str());
385 self.run_make_stage(
386 Stage::PreClean,
387 &pkgdir,
388 &["clean"],
389 RunAs::Root,
390 false,
391 )?;
392
393 if !self.pkginfo.depends.is_empty() {
395 callback.stage(Stage::Depends.as_str());
396 let _ = self.write_stage(Stage::Depends);
397 if !self.install_dependencies()? {
398 return Ok(PkgBuildResult::Failed);
399 }
400 }
401
402 callback.stage(Stage::Checksum.as_str());
404 if !self.run_make_stage(
405 Stage::Checksum,
406 &pkgdir,
407 &["checksum"],
408 RunAs::Root,
409 true,
410 )? {
411 return Ok(PkgBuildResult::Failed);
412 }
413
414 callback.stage(Stage::Configure.as_str());
416 let configure_log = self.logdir.join("configure.log");
417 if !self.run_usergroup_if_needed(
418 Stage::Configure,
419 &pkgdir,
420 &configure_log,
421 )? {
422 return Ok(PkgBuildResult::Failed);
423 }
424 if !self.run_make_stage(
425 Stage::Configure,
426 &pkgdir,
427 &["configure"],
428 self.build_run_as(),
429 true,
430 )? {
431 return Ok(PkgBuildResult::Failed);
432 }
433
434 callback.stage(Stage::Build.as_str());
436 let build_log = self.logdir.join("build.log");
437 if !self.run_usergroup_if_needed(Stage::Build, &pkgdir, &build_log)? {
438 return Ok(PkgBuildResult::Failed);
439 }
440 if !self.run_make_stage(
441 Stage::Build,
442 &pkgdir,
443 &["all"],
444 self.build_run_as(),
445 true,
446 )? {
447 return Ok(PkgBuildResult::Failed);
448 }
449
450 callback.stage(Stage::Install.as_str());
452 let install_log = self.logdir.join("install.log");
453 if !self.run_usergroup_if_needed(
454 Stage::Install,
455 &pkgdir,
456 &install_log,
457 )? {
458 return Ok(PkgBuildResult::Failed);
459 }
460 if !self.run_make_stage(
461 Stage::Install,
462 &pkgdir,
463 &["stage-install"],
464 self.build_run_as(),
465 true,
466 )? {
467 return Ok(PkgBuildResult::Failed);
468 }
469
470 callback.stage(Stage::Package.as_str());
472 if !self.run_make_stage(
473 Stage::Package,
474 &pkgdir,
475 &["stage-package-create"],
476 RunAs::Root,
477 true,
478 )? {
479 return Ok(PkgBuildResult::Failed);
480 }
481
482 let pkgfile = self.get_make_var(&pkgdir, "STAGE_PKGFILE")?;
484
485 let is_bootstrap = self.pkginfo.bootstrap_pkg.as_deref() == Some("yes");
487 if !is_bootstrap {
488 if !self.pkg_add(&pkgfile)? {
489 return Ok(PkgBuildResult::Failed);
490 }
491
492 callback.stage(Stage::Deinstall.as_str());
494 let _ = self.write_stage(Stage::Deinstall);
495 if !self.pkg_delete(pkgname)? {
496 return Ok(PkgBuildResult::Failed);
497 }
498 }
499
500 let packages =
502 self.config.packages().context("pkgsrc.packages not configured")?;
503 let packages_dir = packages.join("All");
504 fs::create_dir_all(&packages_dir)?;
505 let dest = packages_dir.join(
506 Path::new(&pkgfile)
507 .file_name()
508 .context("Invalid package file path")?,
509 );
510 let host_pkgfile = if self.sandbox.enabled() {
512 self.sandbox
513 .path(self.sandbox_id)
514 .join(pkgfile.trim_start_matches('/'))
515 } else {
516 PathBuf::from(&pkgfile)
517 };
518 fs::copy(&host_pkgfile, &dest)?;
519
520 callback.stage(Stage::Clean.as_str());
522 let _ = self.run_make_stage(
523 Stage::Clean,
524 &pkgdir,
525 &["clean"],
526 RunAs::Root,
527 false,
528 );
529
530 let _ = fs::remove_dir_all(&self.logdir);
532
533 Ok(PkgBuildResult::Success)
534 }
535
536 fn build_run_as(&self) -> RunAs {
538 if self.build_user.is_some() { RunAs::User } else { RunAs::Root }
539 }
540
541 fn write_stage(&self, stage: Stage) -> anyhow::Result<()> {
543 let stage_file = self.logdir.join(".stage");
544 fs::write(&stage_file, stage.as_str())?;
545 Ok(())
546 }
547
548 fn run_make_stage(
550 &self,
551 stage: Stage,
552 pkgdir: &Path,
553 targets: &[&str],
554 run_as: RunAs,
555 include_make_flags: bool,
556 ) -> anyhow::Result<bool> {
557 let _ = self.write_stage(stage);
559
560 let logfile = self.logdir.join(format!("{}.log", stage.as_str()));
561 let work_log = self.logdir.join("work.log");
562
563 let owned_args =
564 self.make_args(pkgdir, targets, include_make_flags, &work_log);
565
566 let args: Vec<&str> = owned_args.iter().map(|s| s.as_str()).collect();
568
569 debug!(stage = stage.as_str(), targets = ?targets, "Running make stage");
570
571 let status = self.run_command_logged(
572 self.config.make(),
573 &args,
574 run_as,
575 &logfile,
576 )?;
577
578 Ok(status.success())
579 }
580
581 fn run_command_logged(
583 &self,
584 cmd: &Path,
585 args: &[&str],
586 run_as: RunAs,
587 logfile: &Path,
588 ) -> anyhow::Result<ExitStatus> {
589 self.run_command_logged_with_env(cmd, args, run_as, logfile, &[])
590 }
591
592 fn run_command_logged_with_env(
593 &self,
594 cmd: &Path,
595 args: &[&str],
596 run_as: RunAs,
597 logfile: &Path,
598 extra_envs: &[(&str, &str)],
599 ) -> anyhow::Result<ExitStatus> {
600 use std::io::{BufRead, BufReader, Write};
601
602 let mut log =
603 OpenOptions::new().create(true).append(true).open(logfile)?;
604
605 let _ = writeln!(log, "=> {:?} {:?}", cmd, args);
607 let _ = log.flush();
608
609 if let Some(ref output_tx) = self.output_tx {
612 let shell_cmd =
615 self.build_shell_command(cmd, args, run_as, extra_envs);
616 let mut child = self
617 .sandbox
618 .command(self.sandbox_id, Path::new("/bin/sh"))
619 .arg("-c")
620 .arg(&shell_cmd)
621 .stdout(Stdio::piped())
622 .stderr(Stdio::null())
623 .spawn()
624 .context("Failed to spawn shell command")?;
625
626 let stdout = child.stdout.take().unwrap();
627 let output_tx = output_tx.clone();
628 let sandbox_id = self.sandbox_id;
629
630 let tee_handle = std::thread::spawn(move || {
633 let mut reader = BufReader::new(stdout);
634 let mut buf = Vec::new();
635 let mut batch = Vec::with_capacity(50);
636 let mut last_send = Instant::now();
637 let send_interval = Duration::from_millis(100);
638
639 loop {
640 buf.clear();
641 match reader.read_until(b'\n', &mut buf) {
642 Ok(0) => break,
643 Ok(_) => {}
644 Err(_) => break,
645 };
646 let _ = log.write_all(&buf);
648 let line = String::from_utf8_lossy(&buf);
650 let line = line.trim_end_matches('\n').to_string();
651 batch.push(line);
652
653 if last_send.elapsed() >= send_interval || batch.len() >= 50
655 {
656 let _ = output_tx.send(ChannelCommand::OutputLines(
657 sandbox_id,
658 std::mem::take(&mut batch),
659 ));
660 last_send = Instant::now();
661 }
662 }
663
664 if !batch.is_empty() {
666 let _ = output_tx
667 .send(ChannelCommand::OutputLines(sandbox_id, batch));
668 }
669 });
670
671 let status = child.wait()?;
673
674 let _ = tee_handle.join();
676
677 trace!(cmd = ?cmd, status = ?status, "Command completed");
678 Ok(status)
679 } else {
680 let status =
681 self.spawn_command_to_file(cmd, args, run_as, extra_envs, log)?;
682 trace!(cmd = ?cmd, status = ?status, "Command completed");
683 Ok(status)
684 }
685 }
686
687 fn spawn_command_to_file(
689 &self,
690 cmd: &Path,
691 args: &[&str],
692 run_as: RunAs,
693 extra_envs: &[(&str, &str)],
694 log: File,
695 ) -> anyhow::Result<ExitStatus> {
696 let log_err = log.try_clone()?;
698
699 match run_as {
700 RunAs::Root => {
701 let mut command = self.sandbox.command(self.sandbox_id, cmd);
702 command.args(args);
703 self.apply_envs(&mut command, extra_envs);
704 command
705 .stdout(Stdio::from(log))
706 .stderr(Stdio::from(log_err))
707 .status()
708 .with_context(|| format!("Failed to run {}", cmd.display()))
709 }
710 RunAs::User => {
711 let user = self.build_user.as_ref().unwrap();
712 let mut parts = Vec::with_capacity(args.len() + 1);
713 parts.push(cmd.display().to_string());
714 parts.extend(args.iter().map(|arg| arg.to_string()));
715 let inner_cmd = parts
716 .iter()
717 .map(|part| Self::shell_escape(part))
718 .collect::<Vec<_>>()
719 .join(" ");
720 let mut command =
721 self.sandbox.command(self.sandbox_id, Path::new("su"));
722 command.arg(user).arg("-c").arg(&inner_cmd);
723 self.apply_envs(&mut command, extra_envs);
724 command
725 .stdout(Stdio::from(log))
726 .stderr(Stdio::from(log_err))
727 .status()
728 .context("Failed to run su command")
729 }
730 }
731 }
732
733 fn get_make_var(
735 &self,
736 pkgdir: &Path,
737 varname: &str,
738 ) -> anyhow::Result<String> {
739 let mut cmd = self.sandbox.command(self.sandbox_id, self.config.make());
740 self.apply_envs(&mut cmd, &[]);
741
742 let work_log = self.logdir.join("work.log");
743 let make_args = self.make_args(
744 pkgdir,
745 &["show-var", &format!("VARNAME={}", varname)],
746 true,
747 &work_log,
748 );
749
750 let bob_log = File::options()
751 .create(true)
752 .append(true)
753 .open(self.logdir.join("bob.log"))?;
754 let output =
755 cmd.args(&make_args).stderr(Stdio::from(bob_log)).output()?;
756
757 if !output.status.success() {
758 bail!("Failed to get make variable {}", varname);
759 }
760
761 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
762 }
763
764 fn install_dependencies(&self) -> anyhow::Result<bool> {
766 let deps: Vec<String> =
767 self.pkginfo.depends.iter().map(|d| d.to_string()).collect();
768
769 let packages =
770 self.config.packages().context("pkgsrc.packages not configured")?;
771 let pkg_path = packages.join("All");
772 let logfile = self.logdir.join("depends.log");
773
774 let mut args = vec![];
775 for dep in &deps {
776 args.push(dep.as_str());
777 }
778
779 let status = self.run_pkg_add_with_path(&args, &pkg_path, &logfile)?;
780 Ok(status.success())
781 }
782
783 fn run_pkg_add_with_path(
785 &self,
786 packages: &[&str],
787 pkg_path: &Path,
788 logfile: &Path,
789 ) -> anyhow::Result<ExitStatus> {
790 let pkgtools =
791 self.config.pkgtools().context("pkgsrc.pkgtools not configured")?;
792 let pkg_add = pkgtools.join("pkg_add");
793 let pkg_path_value = pkg_path.to_string_lossy().to_string();
794 let extra_envs = [("PKG_PATH", pkg_path_value.as_str())];
795
796 self.run_command_logged_with_env(
797 &pkg_add,
798 packages,
799 RunAs::Root,
800 logfile,
801 &extra_envs,
802 )
803 }
804
805 fn pkg_add(&self, pkgfile: &str) -> anyhow::Result<bool> {
807 let pkgtools =
808 self.config.pkgtools().context("pkgsrc.pkgtools not configured")?;
809 let pkg_add = pkgtools.join("pkg_add");
810 let logfile = self.logdir.join("package.log");
811
812 let status = self.run_command_logged(
813 &pkg_add,
814 &[pkgfile],
815 RunAs::Root,
816 &logfile,
817 )?;
818
819 Ok(status.success())
820 }
821
822 fn pkg_delete(&self, pkgname: &str) -> anyhow::Result<bool> {
824 let pkgtools =
825 self.config.pkgtools().context("pkgsrc.pkgtools not configured")?;
826 let pkg_delete = pkgtools.join("pkg_delete");
827 let logfile = self.logdir.join("deinstall.log");
828
829 let status = self.run_command_logged(
830 &pkg_delete,
831 &[pkgname],
832 RunAs::Root,
833 &logfile,
834 )?;
835
836 Ok(status.success())
837 }
838
839 fn run_usergroup_if_needed(
841 &self,
842 stage: Stage,
843 pkgdir: &Path,
844 logfile: &Path,
845 ) -> anyhow::Result<bool> {
846 let usergroup_phase =
847 self.pkginfo.usergroup_phase.as_deref().unwrap_or("");
848
849 let should_run = match stage {
850 Stage::Configure => usergroup_phase.ends_with("configure"),
851 Stage::Build => usergroup_phase.ends_with("build"),
852 Stage::Install => usergroup_phase == "pre-install",
853 _ => false,
854 };
855
856 if !should_run {
857 return Ok(true);
858 }
859
860 let mut args = vec!["-C", pkgdir.to_str().unwrap(), "create-usergroup"];
861 if stage == Stage::Configure {
862 args.push("clean");
863 }
864
865 let status = self.run_command_logged(
866 self.config.make(),
867 &args,
868 RunAs::Root,
869 logfile,
870 )?;
871 Ok(status.success())
872 }
873
874 fn make_args(
875 &self,
876 pkgdir: &Path,
877 targets: &[&str],
878 include_make_flags: bool,
879 work_log: &Path,
880 ) -> Vec<String> {
881 let mut owned_args: Vec<String> =
882 vec!["-C".to_string(), pkgdir.to_str().unwrap().to_string()];
883 owned_args.extend(targets.iter().map(|s| s.to_string()));
884
885 if include_make_flags {
886 owned_args.push("BATCH=1".to_string());
887 owned_args.push("DEPENDS_TARGET=/nonexistent".to_string());
888
889 if let Some(ref multi_version) = self.pkginfo.multi_version {
890 for flag in multi_version {
891 owned_args.push(flag.clone());
892 }
893 }
894
895 owned_args.push(format!("WRKLOG={}", work_log.display()));
896 }
897
898 owned_args
899 }
900
901 fn apply_envs(&self, cmd: &mut Command, extra_envs: &[(&str, &str)]) {
902 for (key, value) in &self.envs {
903 cmd.env(key, value);
904 }
905 for (key, value) in extra_envs {
906 cmd.env(key, value);
907 }
908 }
909
910 fn shell_escape(value: &str) -> String {
911 if value.is_empty() {
912 return "''".to_string();
913 }
914 if value
915 .chars()
916 .all(|c| c.is_ascii_alphanumeric() || "-_.,/:=+@".contains(c))
917 {
918 return value.to_string();
919 }
920 let escaped = value.replace('\'', "'\\''");
921 format!("'{}'", escaped)
922 }
923
924 fn build_shell_command(
926 &self,
927 cmd: &Path,
928 args: &[&str],
929 run_as: RunAs,
930 extra_envs: &[(&str, &str)],
931 ) -> String {
932 let mut parts = Vec::new();
933
934 for (key, value) in &self.envs {
936 parts.push(format!("{}={}", key, Self::shell_escape(value)));
937 }
938 for (key, value) in extra_envs {
939 parts.push(format!("{}={}", key, Self::shell_escape(value)));
940 }
941
942 let cmd_str = Self::shell_escape(&cmd.to_string_lossy());
944 let args_str: Vec<String> =
945 args.iter().map(|a| Self::shell_escape(a)).collect();
946
947 match run_as {
948 RunAs::Root => {
949 parts.push(cmd_str);
950 parts.extend(args_str);
951 }
952 RunAs::User => {
953 let user = self.build_user.as_ref().unwrap();
954 let inner_cmd = std::iter::once(cmd_str)
955 .chain(args_str)
956 .collect::<Vec<_>>()
957 .join(" ");
958 parts.push("su".to_string());
959 parts.push(Self::shell_escape(user));
960 parts.push("-c".to_string());
961 parts.push(Self::shell_escape(&inner_cmd));
962 }
963 }
964
965 parts.push("2>&1".to_string());
967 parts.join(" ")
968 }
969}
970
971struct ChannelCallback<'a> {
973 sandbox_id: usize,
974 status_tx: &'a Sender<ChannelCommand>,
975}
976
977impl<'a> ChannelCallback<'a> {
978 fn new(sandbox_id: usize, status_tx: &'a Sender<ChannelCommand>) -> Self {
979 Self { sandbox_id, status_tx }
980 }
981}
982
983impl<'a> BuildCallback for ChannelCallback<'a> {
984 fn stage(&mut self, stage: &str) {
985 let _ = self.status_tx.send(ChannelCommand::StageUpdate(
986 self.sandbox_id,
987 Some(stage.to_string()),
988 ));
989 }
990}
991
992#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
997pub enum BuildOutcome {
998 Success,
1000 Failed(String),
1004 UpToDate,
1007 PreFailed(String),
1012 IndirectFailed(String),
1016 IndirectPreFailed(String),
1020}
1021
1022#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1026pub struct BuildResult {
1027 pub pkgname: PkgName,
1029 pub pkgpath: Option<PkgPath>,
1031 pub outcome: BuildOutcome,
1033 pub duration: Duration,
1035 pub log_dir: Option<PathBuf>,
1040}
1041
1042#[derive(Clone, Debug)]
1062pub struct BuildSummary {
1063 pub duration: Duration,
1065 pub results: Vec<BuildResult>,
1067 pub scan_failed: Vec<ScanFailure>,
1069}
1070
1071impl BuildSummary {
1072 pub fn success_count(&self) -> usize {
1074 self.results
1075 .iter()
1076 .filter(|r| matches!(r.outcome, BuildOutcome::Success))
1077 .count()
1078 }
1079
1080 pub fn failed_count(&self) -> usize {
1082 self.results
1083 .iter()
1084 .filter(|r| matches!(r.outcome, BuildOutcome::Failed(_)))
1085 .count()
1086 }
1087
1088 pub fn up_to_date_count(&self) -> usize {
1090 self.results
1091 .iter()
1092 .filter(|r| matches!(r.outcome, BuildOutcome::UpToDate))
1093 .count()
1094 }
1095
1096 pub fn prefailed_count(&self) -> usize {
1098 self.results
1099 .iter()
1100 .filter(|r| matches!(r.outcome, BuildOutcome::PreFailed(_)))
1101 .count()
1102 }
1103
1104 pub fn indirect_failed_count(&self) -> usize {
1106 self.results
1107 .iter()
1108 .filter(|r| matches!(r.outcome, BuildOutcome::IndirectFailed(_)))
1109 .count()
1110 }
1111
1112 pub fn indirect_prefailed_count(&self) -> usize {
1114 self.results
1115 .iter()
1116 .filter(|r| matches!(r.outcome, BuildOutcome::IndirectPreFailed(_)))
1117 .count()
1118 }
1119
1120 pub fn scan_failed_count(&self) -> usize {
1122 self.scan_failed.len()
1123 }
1124
1125 pub fn failed(&self) -> Vec<&BuildResult> {
1127 self.results
1128 .iter()
1129 .filter(|r| matches!(r.outcome, BuildOutcome::Failed(_)))
1130 .collect()
1131 }
1132
1133 pub fn succeeded(&self) -> Vec<&BuildResult> {
1135 self.results
1136 .iter()
1137 .filter(|r| matches!(r.outcome, BuildOutcome::Success))
1138 .collect()
1139 }
1140
1141 pub fn up_to_date(&self) -> Vec<&BuildResult> {
1143 self.results
1144 .iter()
1145 .filter(|r| matches!(r.outcome, BuildOutcome::UpToDate))
1146 .collect()
1147 }
1148
1149 pub fn prefailed(&self) -> Vec<&BuildResult> {
1151 self.results
1152 .iter()
1153 .filter(|r| matches!(r.outcome, BuildOutcome::PreFailed(_)))
1154 .collect()
1155 }
1156
1157 pub fn indirect_failed(&self) -> Vec<&BuildResult> {
1159 self.results
1160 .iter()
1161 .filter(|r| matches!(r.outcome, BuildOutcome::IndirectFailed(_)))
1162 .collect()
1163 }
1164
1165 pub fn indirect_prefailed(&self) -> Vec<&BuildResult> {
1167 self.results
1168 .iter()
1169 .filter(|r| matches!(r.outcome, BuildOutcome::IndirectPreFailed(_)))
1170 .collect()
1171 }
1172}
1173
1174#[derive(Clone, Debug, Default)]
1176pub struct BuildOptions {
1177 pub force_rebuild: bool,
1179}
1180
1181#[derive(Debug, Default)]
1182pub struct Build {
1183 config: Config,
1185 sandbox: Sandbox,
1187 scanpkgs: IndexMap<PkgName, ResolvedIndex>,
1189 cached: IndexMap<PkgName, BuildResult>,
1191 options: BuildOptions,
1193}
1194
1195#[derive(Debug)]
1196struct PackageBuild {
1197 id: usize,
1198 config: Config,
1199 pkginfo: ResolvedIndex,
1200 sandbox: Sandbox,
1201 options: BuildOptions,
1202}
1203
1204struct MakeQuery<'a> {
1206 config: &'a Config,
1207 sandbox: &'a Sandbox,
1208 sandbox_id: usize,
1209 pkgpath: &'a PkgPath,
1210 env: &'a HashMap<String, String>,
1211}
1212
1213impl<'a> MakeQuery<'a> {
1214 fn new(
1215 config: &'a Config,
1216 sandbox: &'a Sandbox,
1217 sandbox_id: usize,
1218 pkgpath: &'a PkgPath,
1219 env: &'a HashMap<String, String>,
1220 ) -> Self {
1221 Self { config, sandbox, sandbox_id, pkgpath, env }
1222 }
1223
1224 fn var(&self, name: &str) -> Option<String> {
1226 let pkgdir = self.config.pkgsrc().join(self.pkgpath.as_path());
1227
1228 let mut cmd = self.sandbox.command(self.sandbox_id, self.config.make());
1229 cmd.arg("-C")
1230 .arg(&pkgdir)
1231 .arg("show-var")
1232 .arg(format!("VARNAME={}", name));
1233
1234 for (key, value) in self.env {
1236 cmd.env(key, value);
1237 }
1238
1239 cmd.stderr(Stdio::null());
1240
1241 let output = cmd.output().ok()?;
1242
1243 if !output.status.success() {
1244 return None;
1245 }
1246
1247 let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
1248
1249 if value.is_empty() { None } else { Some(value) }
1250 }
1251
1252 fn var_path(&self, name: &str) -> Option<PathBuf> {
1254 self.var(name).map(PathBuf::from)
1255 }
1256
1257 fn wrkdir(&self) -> Option<PathBuf> {
1259 self.var_path("WRKDIR")
1260 }
1261
1262 #[allow(dead_code)]
1264 fn wrksrc(&self) -> Option<PathBuf> {
1265 self.var_path("WRKSRC")
1266 }
1267
1268 #[allow(dead_code)]
1270 fn destdir(&self) -> Option<PathBuf> {
1271 self.var_path("DESTDIR")
1272 }
1273
1274 #[allow(dead_code)]
1276 fn prefix(&self) -> Option<PathBuf> {
1277 self.var_path("PREFIX")
1278 }
1279
1280 fn resolve_path(&self, path: &Path) -> PathBuf {
1283 if self.sandbox.enabled() {
1284 self.sandbox
1285 .path(self.sandbox_id)
1286 .join(path.strip_prefix("/").unwrap_or(path))
1287 } else {
1288 path.to_path_buf()
1289 }
1290 }
1291}
1292
1293#[derive(Debug)]
1295enum PackageBuildResult {
1296 Success,
1298 Failed,
1300 Skipped,
1302}
1303
1304impl PackageBuild {
1305 fn build(
1306 &self,
1307 status_tx: &Sender<ChannelCommand>,
1308 ) -> anyhow::Result<PackageBuildResult> {
1309 let pkgname = self.pkginfo.pkgname.pkgname();
1310 info!(pkgname = %pkgname,
1311 sandbox_id = self.id,
1312 "Starting package build"
1313 );
1314
1315 let Some(pkgpath) = &self.pkginfo.pkg_location else {
1316 error!(pkgname = %pkgname, "Could not get PKGPATH for package");
1317 bail!("Could not get PKGPATH for {}", pkgname);
1318 };
1319
1320 let logdir = self.config.logdir();
1321
1322 let pkg_env = match self.config.get_pkg_env(&self.pkginfo) {
1324 Ok(env) => env,
1325 Err(e) => {
1326 error!(pkgname = %pkgname, error = %e, "Failed to get env from Lua config");
1327 HashMap::new()
1328 }
1329 };
1330
1331 let mut envs = self.config.script_env();
1332 for (key, value) in &pkg_env {
1333 envs.push((key.clone(), value.clone()));
1334 }
1335
1336 let patterns = self.config.save_wrkdir_patterns();
1337
1338 if let Some(pre_build) = self.config.script("pre-build") {
1340 debug!(pkgname = %pkgname, "Running pre-build script");
1341 let child = self.sandbox.execute(
1342 self.id,
1343 pre_build,
1344 envs.clone(),
1345 None,
1346 None,
1347 )?;
1348 let output = child
1349 .wait_with_output()
1350 .context("Failed to wait for pre-build")?;
1351 if !output.status.success() {
1352 warn!(pkgname = %pkgname, exit_code = ?output.status.code(), "pre-build script failed");
1353 }
1354 }
1355
1356 let builder = PkgBuilder::new(
1358 &self.config,
1359 &self.sandbox,
1360 self.id,
1361 &self.pkginfo,
1362 envs.clone(),
1363 Some(status_tx.clone()),
1364 &self.options,
1365 );
1366
1367 let mut callback = ChannelCallback::new(self.id, status_tx);
1368 let result = builder.build(&mut callback);
1369
1370 let _ = status_tx.send(ChannelCommand::StageUpdate(self.id, None));
1372
1373 let result = match &result {
1374 Ok(PkgBuildResult::Success) => {
1375 info!(pkgname = %pkgname, "package build completed successfully");
1376 PackageBuildResult::Success
1377 }
1378 Ok(PkgBuildResult::Skipped) => {
1379 info!(pkgname = %pkgname, "package build skipped (up-to-date)");
1380 PackageBuildResult::Skipped
1381 }
1382 Ok(PkgBuildResult::Failed) => {
1383 error!(pkgname = %pkgname, "package build failed");
1384 let _ = status_tx.send(ChannelCommand::StageUpdate(
1386 self.id,
1387 Some("cleanup".to_string()),
1388 ));
1389 debug!(pkgname = %pkgname, "Calling kill_processes_by_id");
1393 let kill_start = Instant::now();
1394 self.sandbox.kill_processes_by_id(self.id);
1395 debug!(pkgname = %pkgname, elapsed_ms = kill_start.elapsed().as_millis(), "kill_processes_by_id completed");
1396 if !patterns.is_empty() {
1398 debug!(pkgname = %pkgname, "Calling save_wrkdir_files");
1399 let save_start = Instant::now();
1400 self.save_wrkdir_files(
1401 pkgname, pkgpath, logdir, patterns, &pkg_env,
1402 );
1403 debug!(pkgname = %pkgname, elapsed_ms = save_start.elapsed().as_millis(), "save_wrkdir_files completed");
1404 debug!(pkgname = %pkgname, "Calling run_clean");
1405 let clean_start = Instant::now();
1406 self.run_clean(pkgpath, &envs);
1407 debug!(pkgname = %pkgname, elapsed_ms = clean_start.elapsed().as_millis(), "run_clean completed");
1408 } else {
1409 debug!(pkgname = %pkgname, "Calling run_clean (no patterns)");
1410 let clean_start = Instant::now();
1411 self.run_clean(pkgpath, &envs);
1412 debug!(pkgname = %pkgname, elapsed_ms = clean_start.elapsed().as_millis(), "run_clean completed");
1413 }
1414 PackageBuildResult::Failed
1415 }
1416 Err(e) => {
1417 error!(pkgname = %pkgname, error = %e, "package build error");
1418 let _ = status_tx.send(ChannelCommand::StageUpdate(
1420 self.id,
1421 Some("cleanup".to_string()),
1422 ));
1423 debug!(pkgname = %pkgname, "Calling kill_processes_by_id");
1427 let kill_start = Instant::now();
1428 self.sandbox.kill_processes_by_id(self.id);
1429 debug!(pkgname = %pkgname, elapsed_ms = kill_start.elapsed().as_millis(), "kill_processes_by_id completed");
1430 if !patterns.is_empty() {
1432 debug!(pkgname = %pkgname, "Calling save_wrkdir_files");
1433 let save_start = Instant::now();
1434 self.save_wrkdir_files(
1435 pkgname, pkgpath, logdir, patterns, &pkg_env,
1436 );
1437 debug!(pkgname = %pkgname, elapsed_ms = save_start.elapsed().as_millis(), "save_wrkdir_files completed");
1438 debug!(pkgname = %pkgname, "Calling run_clean");
1439 let clean_start = Instant::now();
1440 self.run_clean(pkgpath, &envs);
1441 debug!(pkgname = %pkgname, elapsed_ms = clean_start.elapsed().as_millis(), "run_clean completed");
1442 } else {
1443 debug!(pkgname = %pkgname, "Calling run_clean (no patterns)");
1444 let clean_start = Instant::now();
1445 self.run_clean(pkgpath, &envs);
1446 debug!(pkgname = %pkgname, elapsed_ms = clean_start.elapsed().as_millis(), "run_clean completed");
1447 }
1448 PackageBuildResult::Failed
1449 }
1450 };
1451
1452 if let Some(post_build) = self.config.script("post-build") {
1454 debug!(pkgname = %pkgname, script = %post_build.display(), "Running post-build script");
1455 match self.sandbox.execute(self.id, post_build, envs, None, None) {
1456 Ok(child) => {
1457 debug!(pkgname = %pkgname, pid = ?child.id(), "post-build spawned, waiting");
1458 match child.wait_with_output() {
1459 Ok(output) => {
1460 debug!(pkgname = %pkgname, exit_code = ?output.status.code(), "post-build completed");
1461 if !output.status.success() {
1462 warn!(pkgname = %pkgname, exit_code = ?output.status.code(), "post-build script failed");
1463 }
1464 }
1465 Err(e) => {
1466 warn!(pkgname = %pkgname, error = %e, "Failed to wait for post-build");
1467 }
1468 }
1469 }
1470 Err(e) => {
1471 warn!(pkgname = %pkgname, error = %e, "Failed to spawn post-build script");
1472 }
1473 }
1474 }
1475
1476 Ok(result)
1477 }
1478
1479 fn save_wrkdir_files(
1481 &self,
1482 pkgname: &str,
1483 pkgpath: &PkgPath,
1484 logdir: &Path,
1485 patterns: &[String],
1486 pkg_env: &HashMap<String, String>,
1487 ) {
1488 let make = MakeQuery::new(
1489 &self.config,
1490 &self.sandbox,
1491 self.id,
1492 pkgpath,
1493 pkg_env,
1494 );
1495
1496 let wrkdir = match make.wrkdir() {
1498 Some(w) => w,
1499 None => {
1500 debug!(pkgname = %pkgname, "Could not determine WRKDIR, skipping file save");
1501 return;
1502 }
1503 };
1504
1505 let wrkdir_path = make.resolve_path(&wrkdir);
1507
1508 if !wrkdir_path.exists() {
1509 debug!(pkgname = %pkgname,
1510 wrkdir = %wrkdir_path.display(),
1511 "WRKDIR does not exist, skipping file save"
1512 );
1513 return;
1514 }
1515
1516 let save_dir = logdir.join(pkgname).join("wrkdir-files");
1517 if let Err(e) = fs::create_dir_all(&save_dir) {
1518 warn!(pkgname = %pkgname,
1519 error = %e,
1520 "Failed to create wrkdir-files directory"
1521 );
1522 return;
1523 }
1524
1525 let compiled_patterns: Vec<Pattern> = patterns
1527 .iter()
1528 .filter_map(|p| {
1529 Pattern::new(p).ok().or_else(|| {
1530 warn!(pattern = %p, "Invalid glob pattern");
1531 None
1532 })
1533 })
1534 .collect();
1535
1536 if compiled_patterns.is_empty() {
1537 return;
1538 }
1539
1540 let mut saved_count = 0;
1542 if let Err(e) = walk_and_save(
1543 &wrkdir_path,
1544 &wrkdir_path,
1545 &save_dir,
1546 &compiled_patterns,
1547 &mut saved_count,
1548 ) {
1549 warn!(pkgname = %pkgname,
1550 error = %e,
1551 "Error while saving wrkdir files"
1552 );
1553 }
1554
1555 if saved_count > 0 {
1556 info!(pkgname = %pkgname,
1557 count = saved_count,
1558 dest = %save_dir.display(),
1559 "Saved wrkdir files"
1560 );
1561 }
1562 }
1563
1564 fn run_clean(&self, pkgpath: &PkgPath, envs: &[(String, String)]) {
1566 let pkgdir = self.config.pkgsrc().join(pkgpath.as_path());
1567
1568 let mut cmd = self.sandbox.command(self.id, self.config.make());
1569 cmd.arg("-C").arg(&pkgdir).arg("clean");
1570 for (key, value) in envs {
1571 cmd.env(key, value);
1572 }
1573 let result = cmd
1574 .stdout(std::process::Stdio::null())
1575 .stderr(std::process::Stdio::null())
1576 .status();
1577
1578 if let Err(e) = result {
1579 debug!(error = %e, "Failed to run bmake clean");
1580 }
1581 }
1582}
1583
1584fn walk_and_save(
1586 base: &Path,
1587 current: &Path,
1588 save_dir: &Path,
1589 patterns: &[Pattern],
1590 saved_count: &mut usize,
1591) -> std::io::Result<()> {
1592 if !current.is_dir() {
1593 return Ok(());
1594 }
1595
1596 for entry in fs::read_dir(current)? {
1597 let entry = entry?;
1598 let path = entry.path();
1599
1600 if path.is_dir() {
1601 walk_and_save(base, &path, save_dir, patterns, saved_count)?;
1602 } else if path.is_file() {
1603 let rel_path = path.strip_prefix(base).unwrap_or(&path);
1605 let rel_str = rel_path.to_string_lossy();
1606
1607 for pattern in patterns {
1609 if pattern.matches(&rel_str)
1610 || pattern.matches(
1611 path.file_name()
1612 .unwrap_or_default()
1613 .to_string_lossy()
1614 .as_ref(),
1615 )
1616 {
1617 let dest_path = save_dir.join(rel_path);
1619 if let Some(parent) = dest_path.parent() {
1620 fs::create_dir_all(parent)?;
1621 }
1622
1623 if let Err(e) = fs::copy(&path, &dest_path) {
1625 warn!(src = %path.display(),
1626 dest = %dest_path.display(),
1627 error = %e,
1628 "Failed to copy file"
1629 );
1630 } else {
1631 debug!(src = %path.display(),
1632 dest = %dest_path.display(),
1633 "Saved wrkdir file"
1634 );
1635 *saved_count += 1;
1636 }
1637 break; }
1639 }
1640 }
1641 }
1642
1643 Ok(())
1644}
1645
1646#[derive(Debug)]
1650enum ChannelCommand {
1651 ClientReady(usize),
1655 ComeBackLater,
1659 JobData(Box<PackageBuild>),
1663 JobSuccess(PkgName, Duration),
1667 JobFailed(PkgName, Duration),
1671 JobSkipped(PkgName),
1675 JobError((PkgName, Duration, anyhow::Error)),
1679 Quit,
1683 Shutdown,
1687 StageUpdate(usize, Option<String>),
1691 OutputLines(usize, Vec<String>),
1695}
1696
1697#[derive(Debug)]
1701enum BuildStatus {
1702 Available(PkgName),
1706 NoneAvailable,
1711 Done,
1715}
1716
1717#[derive(Clone, Debug)]
1718struct BuildJobs {
1719 scanpkgs: IndexMap<PkgName, ResolvedIndex>,
1720 incoming: HashMap<PkgName, HashSet<PkgName>>,
1721 reverse_deps: HashMap<PkgName, HashSet<PkgName>>,
1724 effective_weights: HashMap<PkgName, usize>,
1727 running: HashSet<PkgName>,
1728 done: HashSet<PkgName>,
1729 failed: HashSet<PkgName>,
1730 results: Vec<BuildResult>,
1731 logdir: PathBuf,
1732 #[allow(dead_code)]
1734 cached_count: usize,
1735}
1736
1737impl BuildJobs {
1738 fn mark_success(&mut self, pkgname: &PkgName, duration: Duration) {
1742 self.mark_done(pkgname, BuildOutcome::Success, duration);
1743 }
1744
1745 fn mark_up_to_date(&mut self, pkgname: &PkgName) {
1746 self.mark_done(pkgname, BuildOutcome::UpToDate, Duration::ZERO);
1747 }
1748
1749 fn mark_done(
1753 &mut self,
1754 pkgname: &PkgName,
1755 outcome: BuildOutcome,
1756 duration: Duration,
1757 ) {
1758 for dep in self.incoming.values_mut() {
1764 if dep.contains(pkgname) {
1765 dep.remove(pkgname);
1766 }
1767 }
1768 self.done.insert(pkgname.clone());
1773
1774 let scanpkg = self.scanpkgs.get(pkgname);
1776 let log_dir = Some(self.logdir.join(pkgname.pkgname()));
1777 self.results.push(BuildResult {
1778 pkgname: pkgname.clone(),
1779 pkgpath: scanpkg.and_then(|s| s.pkg_location.clone()),
1780 outcome,
1781 duration,
1782 log_dir,
1783 });
1784 }
1785
1786 fn mark_failure(&mut self, pkgname: &PkgName, duration: Duration) {
1790 debug!(pkgname = %pkgname.pkgname(), "mark_failure called");
1791 let start = std::time::Instant::now();
1792 let mut broken: HashSet<PkgName> = HashSet::new();
1793 let mut to_check: Vec<PkgName> = vec![];
1794 to_check.push(pkgname.clone());
1795 loop {
1801 let Some(badpkg) = to_check.pop() else {
1803 break;
1804 };
1805 if broken.contains(&badpkg) {
1807 continue;
1808 }
1809 if let Some(dependents) = self.reverse_deps.get(&badpkg) {
1811 for pkg in dependents {
1812 to_check.push(pkg.clone());
1813 }
1814 }
1815 broken.insert(badpkg);
1816 }
1817 debug!(pkgname = %pkgname.pkgname(), broken_count = broken.len(), elapsed_ms = start.elapsed().as_millis(), "mark_failure found broken packages");
1818 let is_original = |p: &PkgName| p == pkgname;
1825 for pkg in broken {
1826 self.incoming.remove(&pkg);
1827 self.failed.insert(pkg.clone());
1828
1829 let scanpkg = self.scanpkgs.get(&pkg);
1831 let log_dir = Some(self.logdir.join(pkg.pkgname()));
1832 let (outcome, dur) = if is_original(&pkg) {
1833 (BuildOutcome::Failed("Build failed".to_string()), duration)
1834 } else {
1835 (
1836 BuildOutcome::IndirectFailed(pkgname.pkgname().to_string()),
1837 Duration::ZERO,
1838 )
1839 };
1840 self.results.push(BuildResult {
1841 pkgname: pkg,
1842 pkgpath: scanpkg.and_then(|s| s.pkg_location.clone()),
1843 outcome,
1844 duration: dur,
1845 log_dir,
1846 });
1847 }
1848 debug!(pkgname = %pkgname.pkgname(), total_results = self.results.len(), elapsed_ms = start.elapsed().as_millis(), "mark_failure completed");
1849 }
1850
1851 #[allow(dead_code)]
1856 fn mark_prefailed(&mut self, pkgname: &PkgName, reason: String) {
1857 let mut broken: HashSet<PkgName> = HashSet::new();
1858 let mut to_check: Vec<PkgName> = vec![];
1859 to_check.push(pkgname.clone());
1860
1861 loop {
1862 let Some(badpkg) = to_check.pop() else {
1863 break;
1864 };
1865 if broken.contains(&badpkg) {
1866 continue;
1867 }
1868 for (pkg, deps) in &self.incoming {
1869 if deps.contains(&badpkg) {
1870 to_check.push(pkg.clone());
1871 }
1872 }
1873 broken.insert(badpkg);
1874 }
1875
1876 let is_original = |p: &PkgName| p == pkgname;
1877 for pkg in broken {
1878 self.incoming.remove(&pkg);
1879 self.failed.insert(pkg.clone());
1880
1881 let scanpkg = self.scanpkgs.get(&pkg);
1882 let log_dir = Some(self.logdir.join(pkg.pkgname()));
1883 let outcome = if is_original(&pkg) {
1884 BuildOutcome::PreFailed(reason.clone())
1885 } else {
1886 BuildOutcome::IndirectPreFailed(pkgname.pkgname().to_string())
1887 };
1888 self.results.push(BuildResult {
1889 pkgname: pkg,
1890 pkgpath: scanpkg.and_then(|s| s.pkg_location.clone()),
1891 outcome,
1892 duration: Duration::ZERO,
1893 log_dir,
1894 });
1895 }
1896 }
1897
1898 fn get_next_build(&self) -> BuildStatus {
1902 if self.incoming.is_empty() {
1906 return BuildStatus::Done;
1907 }
1908
1909 let mut pkgs: Vec<(PkgName, usize)> = self
1914 .incoming
1915 .iter()
1916 .filter(|(_, v)| v.is_empty())
1917 .map(|(k, _)| {
1918 (k.clone(), *self.effective_weights.get(k).unwrap_or(&100))
1919 })
1920 .collect();
1921
1922 if pkgs.is_empty() {
1928 return BuildStatus::NoneAvailable;
1929 }
1930
1931 pkgs.sort_by_key(|&(_, weight)| std::cmp::Reverse(weight));
1935 BuildStatus::Available(pkgs[0].0.clone())
1936 }
1937}
1938
1939impl Build {
1940 pub fn new(
1941 config: &Config,
1942 scanpkgs: IndexMap<PkgName, ResolvedIndex>,
1943 options: BuildOptions,
1944 ) -> Build {
1945 let sandbox = Sandbox::new(config);
1946 info!(
1947 package_count = scanpkgs.len(),
1948 sandbox_enabled = sandbox.enabled(),
1949 build_threads = config.build_threads(),
1950 ?options,
1951 "Creating new Build instance"
1952 );
1953 for (pkgname, index) in &scanpkgs {
1954 debug!(pkgname = %pkgname.pkgname(),
1955 pkgpath = ?index.pkg_location,
1956 depends_count = index.depends.len(),
1957 depends = ?index.depends.iter().map(|d| d.pkgname()).collect::<Vec<_>>(),
1958 "Package in build queue"
1959 );
1960 }
1961 Build {
1962 config: config.clone(),
1963 sandbox,
1964 scanpkgs,
1965 cached: IndexMap::new(),
1966 options,
1967 }
1968 }
1969
1970 pub fn load_cached_from_db(
1975 &mut self,
1976 db: &crate::db::Database,
1977 ) -> anyhow::Result<usize> {
1978 let mut count = 0;
1979 for pkgname in self.scanpkgs.keys() {
1980 if let Some(pkg) = db.get_package_by_name(pkgname.pkgname())? {
1981 if let Some(result) = db.get_build_result(pkg.id)? {
1982 self.cached.insert(pkgname.clone(), result);
1983 count += 1;
1984 }
1985 }
1986 }
1987 if count > 0 {
1988 info!(
1989 cached_count = count,
1990 "Loaded cached build results from database"
1991 );
1992 }
1993 Ok(count)
1994 }
1995
1996 pub fn cached(&self) -> &IndexMap<PkgName, BuildResult> {
1998 &self.cached
1999 }
2000
2001 pub fn start(
2002 &mut self,
2003 ctx: &RunContext,
2004 db: &crate::db::Database,
2005 ) -> anyhow::Result<BuildSummary> {
2006 let started = Instant::now();
2007
2008 info!(package_count = self.scanpkgs.len(), "Build::start() called");
2009
2010 let shutdown_flag = Arc::clone(&ctx.shutdown);
2011
2012 debug!("Populating BuildJobs from scanpkgs");
2016 let mut incoming: HashMap<PkgName, HashSet<PkgName>> = HashMap::new();
2017 let mut reverse_deps: HashMap<PkgName, HashSet<PkgName>> =
2018 HashMap::new();
2019 for (pkgname, index) in &self.scanpkgs {
2020 let mut deps: HashSet<PkgName> = HashSet::new();
2021 for dep in &index.depends {
2022 if !self.scanpkgs.contains_key(dep) {
2027 continue;
2028 }
2029 deps.insert(dep.clone());
2030 reverse_deps
2032 .entry(dep.clone())
2033 .or_default()
2034 .insert(pkgname.clone());
2035 }
2036 trace!(pkgname = %pkgname.pkgname(),
2037 deps_count = deps.len(),
2038 deps = ?deps.iter().map(|d| d.pkgname()).collect::<Vec<_>>(),
2039 "Adding package to incoming build queue"
2040 );
2041 incoming.insert(pkgname.clone(), deps);
2042 }
2043
2044 let mut done: HashSet<PkgName> = HashSet::new();
2048 let mut failed: HashSet<PkgName> = HashSet::new();
2049 let results: Vec<BuildResult> = Vec::new();
2050 let mut cached_count = 0usize;
2051
2052 for (pkgname, result) in &self.cached {
2053 match result.outcome {
2054 BuildOutcome::Success | BuildOutcome::UpToDate => {
2055 incoming.remove(pkgname);
2057 done.insert(pkgname.clone());
2058 for deps in incoming.values_mut() {
2060 deps.remove(pkgname);
2061 }
2062 cached_count += 1;
2064 }
2065 BuildOutcome::Failed(_)
2066 | BuildOutcome::PreFailed(_)
2067 | BuildOutcome::IndirectFailed(_)
2068 | BuildOutcome::IndirectPreFailed(_) => {
2069 incoming.remove(pkgname);
2071 failed.insert(pkgname.clone());
2072 cached_count += 1;
2074 }
2075 }
2076 }
2077
2078 loop {
2083 let mut newly_failed: Vec<PkgName> = Vec::new();
2084 for (pkgname, deps) in &incoming {
2085 for dep in deps {
2086 if failed.contains(dep) {
2087 newly_failed.push(pkgname.clone());
2088 break;
2089 }
2090 }
2091 }
2092 if newly_failed.is_empty() {
2093 break;
2094 }
2095 for pkgname in newly_failed {
2096 incoming.remove(&pkgname);
2097 failed.insert(pkgname);
2098 }
2099 }
2100
2101 if cached_count > 0 {
2102 println!("Loaded {} cached build results", cached_count);
2103 }
2104
2105 info!(
2106 incoming_count = incoming.len(),
2107 scanpkgs_count = self.scanpkgs.len(),
2108 cached_count = cached_count,
2109 "BuildJobs populated"
2110 );
2111
2112 if incoming.is_empty() {
2113 return Ok(BuildSummary {
2114 duration: started.elapsed(),
2115 results,
2116 scan_failed: Vec::new(),
2117 });
2118 }
2119
2120 let get_weight = |pkg: &PkgName| -> usize {
2127 self.scanpkgs
2128 .get(pkg)
2129 .and_then(|idx| idx.pbulk_weight.as_ref())
2130 .and_then(|w| w.parse().ok())
2131 .unwrap_or(100)
2132 };
2133
2134 let mut effective_weights: HashMap<PkgName, usize> = HashMap::new();
2135 let mut pending: HashMap<&PkgName, usize> = incoming
2136 .keys()
2137 .map(|p| (p, reverse_deps.get(p).map_or(0, |s| s.len())))
2138 .collect();
2139 let mut queue: VecDeque<&PkgName> =
2140 pending.iter().filter(|(_, c)| **c == 0).map(|(&p, _)| p).collect();
2141 while let Some(pkg) = queue.pop_front() {
2142 let mut total = get_weight(pkg);
2143 if let Some(dependents) = reverse_deps.get(pkg) {
2144 for dep in dependents {
2145 total += effective_weights.get(dep).unwrap_or(&0);
2146 }
2147 }
2148 effective_weights.insert(pkg.clone(), total);
2149 for dep in incoming.get(pkg).iter().flat_map(|s| s.iter()) {
2150 if let Some(c) = pending.get_mut(dep) {
2151 *c -= 1;
2152 if *c == 0 {
2153 queue.push_back(dep);
2154 }
2155 }
2156 }
2157 }
2158
2159 let running: HashSet<PkgName> = HashSet::new();
2160 let logdir = self.config.logdir().clone();
2161 let jobs = BuildJobs {
2162 scanpkgs: self.scanpkgs.clone(),
2163 incoming,
2164 reverse_deps,
2165 effective_weights,
2166 running,
2167 done,
2168 failed,
2169 results,
2170 logdir,
2171 cached_count,
2172 };
2173
2174 if self.sandbox.enabled() {
2176 println!("Creating sandboxes...");
2177 for i in 0..self.config.build_threads() {
2178 if let Err(e) = self.sandbox.create(i) {
2179 for j in (0..=i).rev() {
2181 if let Err(destroy_err) = self.sandbox.destroy(j) {
2182 eprintln!(
2183 "Warning: failed to destroy sandbox {}: {}",
2184 j, destroy_err
2185 );
2186 }
2187 }
2188 return Err(e);
2189 }
2190 }
2191 }
2192
2193 println!("Building packages...");
2194
2195 let progress = Arc::new(Mutex::new(
2197 MultiProgress::new(
2198 "Building",
2199 "Built",
2200 self.scanpkgs.len(),
2201 self.config.build_threads(),
2202 )
2203 .expect("Failed to initialize progress display"),
2204 ));
2205
2206 if cached_count > 0 {
2208 if let Ok(mut p) = progress.lock() {
2209 p.state_mut().cached = cached_count;
2210 }
2211 }
2212
2213 let stop_refresh = Arc::new(AtomicBool::new(false));
2215
2216 let progress_refresh = Arc::clone(&progress);
2218 let stop_flag = Arc::clone(&stop_refresh);
2219 let shutdown_for_refresh = Arc::clone(&shutdown_flag);
2220 let refresh_thread = std::thread::spawn(move || {
2221 while !stop_flag.load(Ordering::Relaxed)
2222 && !shutdown_for_refresh.load(Ordering::SeqCst)
2223 {
2224 if let Ok(mut p) = progress_refresh.lock() {
2225 let _ = p.poll_events();
2227 let _ = p.render_throttled();
2228 }
2229 std::thread::sleep(Duration::from_millis(50));
2230 }
2231 });
2232
2233 let (manager_tx, manager_rx) = mpsc::channel::<ChannelCommand>();
2238
2239 let mut threads = vec![];
2245 let mut clients: HashMap<usize, Sender<ChannelCommand>> =
2246 HashMap::new();
2247 for i in 0..self.config.build_threads() {
2248 let (client_tx, client_rx) = mpsc::channel::<ChannelCommand>();
2249 clients.insert(i, client_tx);
2250 let manager_tx = manager_tx.clone();
2251 let thread = std::thread::spawn(move || {
2252 loop {
2253 if manager_tx.send(ChannelCommand::ClientReady(i)).is_err()
2255 {
2256 break;
2257 }
2258
2259 let Ok(msg) = client_rx.recv() else {
2260 break;
2261 };
2262
2263 match msg {
2264 ChannelCommand::ComeBackLater => {
2265 std::thread::sleep(Duration::from_millis(100));
2266 continue;
2267 }
2268 ChannelCommand::JobData(pkg) => {
2269 let pkgname = pkg.pkginfo.pkgname.clone();
2270 trace!(pkgname = %pkgname.pkgname(), worker = i, "Worker starting build");
2271 let build_start = Instant::now();
2272 let result = pkg.build(&manager_tx);
2273 let duration = build_start.elapsed();
2274 trace!(pkgname = %pkgname.pkgname(), worker = i, elapsed_ms = duration.as_millis(), "Worker build() returned");
2275 match result {
2276 Ok(PackageBuildResult::Success) => {
2277 trace!(pkgname = %pkgname.pkgname(), "Worker sending JobSuccess");
2278 let _ = manager_tx.send(
2279 ChannelCommand::JobSuccess(
2280 pkgname, duration,
2281 ),
2282 );
2283 }
2284 Ok(PackageBuildResult::Skipped) => {
2285 trace!(pkgname = %pkgname.pkgname(), "Worker sending JobSkipped");
2286 let _ = manager_tx.send(
2287 ChannelCommand::JobSkipped(pkgname),
2288 );
2289 }
2290 Ok(PackageBuildResult::Failed) => {
2291 trace!(pkgname = %pkgname.pkgname(), "Worker sending JobFailed");
2292 let _ = manager_tx.send(
2293 ChannelCommand::JobFailed(
2294 pkgname, duration,
2295 ),
2296 );
2297 }
2298 Err(e) => {
2299 trace!(pkgname = %pkgname.pkgname(), "Worker sending JobError");
2300 let _ = manager_tx.send(
2301 ChannelCommand::JobError((
2302 pkgname, duration, e,
2303 )),
2304 );
2305 }
2306 }
2307 continue;
2308 }
2309 ChannelCommand::Quit | ChannelCommand::Shutdown => {
2310 break;
2311 }
2312 _ => todo!(),
2313 }
2314 }
2315 });
2316 threads.push(thread);
2317 }
2318
2319 let config = self.config.clone();
2324 let sandbox = self.sandbox.clone();
2325 let options = self.options.clone();
2326 let progress_clone = Arc::clone(&progress);
2327 let shutdown_for_manager = Arc::clone(&shutdown_flag);
2328 let (results_tx, results_rx) = mpsc::channel::<Vec<BuildResult>>();
2329 let (interrupted_tx, interrupted_rx) = mpsc::channel::<bool>();
2330 let (completed_tx, completed_rx) = mpsc::channel::<BuildResult>();
2332 let manager = std::thread::spawn(move || {
2333 let mut clients = clients.clone();
2334 let config = config.clone();
2335 let sandbox = sandbox.clone();
2336 let mut jobs = jobs.clone();
2337 let mut was_interrupted = false;
2338
2339 let mut thread_packages: HashMap<usize, PkgName> = HashMap::new();
2341
2342 loop {
2343 if shutdown_for_manager.load(Ordering::SeqCst) {
2345 if let Ok(mut p) = progress_clone.lock() {
2347 p.state_mut().suppress();
2348 }
2349 for (_, client) in clients.drain() {
2351 let _ = client.send(ChannelCommand::Shutdown);
2352 }
2353 was_interrupted = true;
2354 break;
2355 }
2356
2357 let command =
2359 match manager_rx.recv_timeout(Duration::from_millis(50)) {
2360 Ok(cmd) => cmd,
2361 Err(mpsc::RecvTimeoutError::Timeout) => continue,
2362 Err(mpsc::RecvTimeoutError::Disconnected) => break,
2363 };
2364
2365 match command {
2366 ChannelCommand::ClientReady(c) => {
2367 let client = clients.get(&c).unwrap();
2368 match jobs.get_next_build() {
2369 BuildStatus::Available(pkg) => {
2370 let pkginfo = jobs.scanpkgs.get(&pkg).unwrap();
2371 jobs.incoming.remove(&pkg);
2372 jobs.running.insert(pkg.clone());
2373
2374 thread_packages.insert(c, pkg.clone());
2376 if let Ok(mut p) = progress_clone.lock() {
2377 p.clear_output_buffer(c);
2378 p.state_mut()
2379 .set_worker_active(c, pkg.pkgname());
2380 let _ = p.render_throttled();
2381 }
2382
2383 let _ = client.send(ChannelCommand::JobData(
2384 Box::new(PackageBuild {
2385 id: c,
2386 config: config.clone(),
2387 pkginfo: pkginfo.clone(),
2388 sandbox: sandbox.clone(),
2389 options: options.clone(),
2390 }),
2391 ));
2392 }
2393 BuildStatus::NoneAvailable => {
2394 if let Ok(mut p) = progress_clone.lock() {
2395 p.clear_output_buffer(c);
2396 p.state_mut().set_worker_idle(c);
2397 let _ = p.render_throttled();
2398 }
2399 let _ =
2400 client.send(ChannelCommand::ComeBackLater);
2401 }
2402 BuildStatus::Done => {
2403 if let Ok(mut p) = progress_clone.lock() {
2404 p.clear_output_buffer(c);
2405 p.state_mut().set_worker_idle(c);
2406 let _ = p.render_throttled();
2407 }
2408 let _ = client.send(ChannelCommand::Quit);
2409 clients.remove(&c);
2410 if clients.is_empty() {
2411 break;
2412 }
2413 }
2414 };
2415 }
2416 ChannelCommand::JobSuccess(pkgname, duration) => {
2417 jobs.mark_success(&pkgname, duration);
2418 jobs.running.remove(&pkgname);
2419
2420 if let Some(result) = jobs.results.last() {
2422 let _ = completed_tx.send(result.clone());
2423 }
2424
2425 if shutdown_for_manager.load(Ordering::SeqCst) {
2427 continue;
2428 }
2429
2430 if let Ok(mut p) = progress_clone.lock() {
2432 let _ = p.print_status(&format!(
2433 " Built {} ({})",
2434 pkgname.pkgname(),
2435 format_duration(duration)
2436 ));
2437 p.state_mut().increment_completed();
2438 for (tid, pkg) in &thread_packages {
2439 if pkg == &pkgname {
2440 p.clear_output_buffer(*tid);
2441 p.state_mut().set_worker_idle(*tid);
2442 break;
2443 }
2444 }
2445 let _ = p.render_throttled();
2446 }
2447 }
2448 ChannelCommand::JobSkipped(pkgname) => {
2449 jobs.mark_up_to_date(&pkgname);
2450 jobs.running.remove(&pkgname);
2451
2452 if let Some(result) = jobs.results.last() {
2454 let _ = completed_tx.send(result.clone());
2455 }
2456
2457 if shutdown_for_manager.load(Ordering::SeqCst) {
2459 continue;
2460 }
2461
2462 if let Ok(mut p) = progress_clone.lock() {
2464 let _ = p.print_status(&format!(
2465 " Skipped {} (up-to-date)",
2466 pkgname.pkgname()
2467 ));
2468 p.state_mut().increment_skipped();
2469 for (tid, pkg) in &thread_packages {
2470 if pkg == &pkgname {
2471 p.clear_output_buffer(*tid);
2472 p.state_mut().set_worker_idle(*tid);
2473 break;
2474 }
2475 }
2476 let _ = p.render_throttled();
2477 }
2478 }
2479 ChannelCommand::JobFailed(pkgname, duration) => {
2480 let results_before = jobs.results.len();
2481 jobs.mark_failure(&pkgname, duration);
2482 jobs.running.remove(&pkgname);
2483
2484 for result in jobs.results.iter().skip(results_before) {
2486 let _ = completed_tx.send(result.clone());
2487 }
2488
2489 if shutdown_for_manager.load(Ordering::SeqCst) {
2491 continue;
2492 }
2493
2494 if let Ok(mut p) = progress_clone.lock() {
2496 let _ = p.print_status(&format!(
2497 " Failed {} ({})",
2498 pkgname.pkgname(),
2499 format_duration(duration)
2500 ));
2501 p.state_mut().increment_failed();
2502 for (tid, pkg) in &thread_packages {
2503 if pkg == &pkgname {
2504 p.clear_output_buffer(*tid);
2505 p.state_mut().set_worker_idle(*tid);
2506 break;
2507 }
2508 }
2509 let _ = p.render_throttled();
2510 }
2511 }
2512 ChannelCommand::JobError((pkgname, duration, e)) => {
2513 let results_before = jobs.results.len();
2514 jobs.mark_failure(&pkgname, duration);
2515 jobs.running.remove(&pkgname);
2516
2517 for result in jobs.results.iter().skip(results_before) {
2519 let _ = completed_tx.send(result.clone());
2520 }
2521
2522 if shutdown_for_manager.load(Ordering::SeqCst) {
2524 tracing::error!(error = %e, pkgname = %pkgname.pkgname(), "Build error");
2525 continue;
2526 }
2527
2528 if let Ok(mut p) = progress_clone.lock() {
2530 let _ = p.print_status(&format!(
2531 " Failed {} ({})",
2532 pkgname.pkgname(),
2533 format_duration(duration)
2534 ));
2535 p.state_mut().increment_failed();
2536 for (tid, pkg) in &thread_packages {
2537 if pkg == &pkgname {
2538 p.clear_output_buffer(*tid);
2539 p.state_mut().set_worker_idle(*tid);
2540 break;
2541 }
2542 }
2543 let _ = p.render_throttled();
2544 }
2545 tracing::error!(error = %e, pkgname = %pkgname.pkgname(), "Build error");
2546 }
2547 ChannelCommand::StageUpdate(tid, stage) => {
2548 if let Ok(mut p) = progress_clone.lock() {
2549 p.state_mut()
2550 .set_worker_stage(tid, stage.as_deref());
2551 let _ = p.render_throttled();
2552 }
2553 }
2554 ChannelCommand::OutputLines(tid, lines) => {
2555 if let Ok(mut p) = progress_clone.lock() {
2556 if let Some(buf) = p.output_buffer_mut(tid) {
2557 for line in lines {
2558 buf.push(line);
2559 }
2560 }
2561 }
2562 }
2563 _ => {}
2564 }
2565 }
2566
2567 debug!(
2569 result_count = jobs.results.len(),
2570 "Manager sending results back"
2571 );
2572 let _ = results_tx.send(jobs.results);
2573 let _ = interrupted_tx.send(was_interrupted);
2574 });
2575
2576 threads.push(manager);
2577 debug!("Waiting for worker threads to complete");
2578 let join_start = Instant::now();
2579 for thread in threads {
2580 thread.join().expect("thread panicked");
2581 }
2582 debug!(
2583 elapsed_ms = join_start.elapsed().as_millis(),
2584 "Worker threads completed"
2585 );
2586
2587 let mut saved_count = 0;
2589 while let Ok(result) = completed_rx.try_recv() {
2590 if let Err(e) = db.store_build_by_name(&result) {
2591 warn!(
2592 pkgname = %result.pkgname.pkgname(),
2593 error = %e,
2594 "Failed to save build result"
2595 );
2596 } else {
2597 saved_count += 1;
2598 }
2599 }
2600 if saved_count > 0 {
2601 debug!(saved_count, "Saved build results to database");
2602 }
2603
2604 stop_refresh.store(true, Ordering::Relaxed);
2606 let _ = refresh_thread.join();
2607
2608 let was_interrupted = interrupted_rx.recv().unwrap_or(false);
2610
2611 if let Ok(mut p) = progress.lock() {
2613 if was_interrupted {
2614 let _ = p.finish_interrupted();
2615 } else {
2616 let _ = p.finish();
2617 }
2618 }
2619
2620 debug!("Collecting results from manager");
2622 let results = results_rx.recv().unwrap_or_default();
2623 debug!(result_count = results.len(), "Collected results from manager");
2624 let summary = BuildSummary {
2625 duration: started.elapsed(),
2626 results,
2627 scan_failed: Vec::new(),
2628 };
2629
2630 if self.sandbox.enabled() {
2631 debug!("Destroying sandboxes");
2632 let destroy_start = Instant::now();
2633 self.sandbox.destroy_all(self.config.build_threads())?;
2634 debug!(
2635 elapsed_ms = destroy_start.elapsed().as_millis(),
2636 "Sandboxes destroyed"
2637 );
2638 }
2639
2640 Ok(summary)
2641 }
2642}