1use std::{
3 collections::{HashMap, HashSet},
4 path::PathBuf,
5 time::{Duration, SystemTime},
6};
7
8pub use cognos::ProgressState;
9use cognos::{Host, Id, OutputName};
10use indexmap::IndexMap;
11
12pub type StorePathId = usize;
14
15pub type DerivationId = usize;
17
18pub type ActivityId = Id;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct StorePath {
24 pub path: PathBuf,
25 pub hash: String,
26 pub name: String,
27}
28
29impl StorePath {
30 #[must_use]
31 pub fn parse(path: &str) -> Option<Self> {
32 if !path.starts_with("/nix/store/") {
33 return None;
34 }
35
36 let path_buf = PathBuf::from(path);
37 let file_name = path_buf.file_name()?.to_str()?;
38
39 let parts: Vec<&str> = file_name.splitn(2, '-').collect();
40 if parts.len() != 2 {
41 return None;
42 }
43
44 Some(Self {
45 path: path_buf.clone(),
46 hash: parts[0].to_string(),
47 name: parts[1].to_string(),
48 })
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct Derivation {
55 pub path: PathBuf,
56 pub name: String,
57}
58
59impl Derivation {
60 #[must_use]
61 pub fn parse(path: &str) -> Option<Self> {
62 let path_buf = PathBuf::from(path);
63 let file_name = path_buf.file_name()?.to_str()?;
64
65 if !file_name.ends_with(".drv") {
66 return None;
67 }
68
69 let name = file_name.strip_suffix(".drv")?;
70 let parts: Vec<&str> = name.splitn(2, '-').collect();
71 let display_name = if parts.len() == 2 {
72 parts[1].to_string()
73 } else {
74 name.to_string()
75 };
76
77 Some(Self {
78 path: path_buf,
79 name: display_name,
80 })
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct TransferInfo {
87 pub start: f64,
88 pub host: Host,
89 pub activity_id: ActivityId,
90 pub bytes_transferred: u64,
91 pub total_bytes: Option<u64>,
92}
93
94#[derive(Debug, Clone)]
96pub struct CompletedTransferInfo {
97 pub start: f64,
98 pub end: f64,
99 pub host: Host,
100 pub total_bytes: u64,
101}
102
103#[derive(Debug, Clone)]
105pub struct StorePathInfo {
106 pub name: StorePath,
107 pub producer: Option<DerivationId>,
108 pub input_for: HashSet<DerivationId>,
109}
110
111#[derive(Debug, Clone)]
113pub struct BuildInfo {
114 pub start: f64,
115 pub host: Host,
116 pub estimate: Option<u64>,
117 pub activity_id: Option<ActivityId>,
118}
119
120#[derive(Debug, Clone)]
122pub struct BuildFail {
123 pub at: f64,
124 pub fail_type: FailType,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum FailType {
130 BuildFailed(i32),
131 Timeout,
132 HashMismatch,
133 DependencyFailed,
134 Unknown,
135}
136
137#[derive(Debug, Clone)]
139pub enum BuildStatus {
140 Unknown,
141 Planned,
142 Building(BuildInfo),
143 Built { info: BuildInfo, end: f64 },
144 Failed { info: BuildInfo, fail: BuildFail },
145}
146
147#[derive(Debug, Clone)]
149pub struct InputDerivation {
150 pub derivation: DerivationId,
151 pub outputs: HashSet<OutputName>,
152}
153
154#[derive(Debug, Clone)]
156pub struct DerivationInfo {
157 pub name: Derivation,
158 pub outputs: HashMap<OutputName, StorePathId>,
159 pub input_derivations: Vec<InputDerivation>,
160 pub input_sources: HashSet<StorePathId>,
161 pub build_status: BuildStatus,
162 pub dependency_summary: DependencySummary,
163 pub cached: bool,
164 pub derivation_parents: HashSet<DerivationId>,
165 pub pname: Option<String>,
166 pub platform: Option<String>,
167}
168
169#[derive(Debug, Clone, Default)]
171pub struct DependencySummary {
172 pub planned_builds: HashSet<DerivationId>,
173 pub running_builds: HashMap<DerivationId, BuildInfo>,
174 pub completed_builds: HashMap<DerivationId, CompletedBuildInfo>,
175 pub failed_builds: HashMap<DerivationId, FailedBuildInfo>,
176 pub planned_downloads: HashSet<StorePathId>,
177 pub completed_downloads: HashMap<StorePathId, CompletedTransferInfo>,
178 pub completed_uploads: HashMap<StorePathId, CompletedTransferInfo>,
179 pub running_downloads: HashMap<StorePathId, TransferInfo>,
180 pub running_uploads: HashMap<StorePathId, TransferInfo>,
181}
182
183impl DependencySummary {
184 pub fn merge(&mut self, other: &Self) {
185 self
186 .planned_builds
187 .extend(other.planned_builds.iter().copied());
188 self
189 .running_builds
190 .extend(other.running_builds.iter().map(|(k, v)| (*k, v.clone())));
191 self
192 .completed_builds
193 .extend(other.completed_builds.iter().map(|(k, v)| (*k, v.clone())));
194 self
195 .failed_builds
196 .extend(other.failed_builds.iter().map(|(k, v)| (*k, v.clone())));
197 self
198 .planned_downloads
199 .extend(other.planned_downloads.iter().copied());
200 self.completed_downloads.extend(
201 other
202 .completed_downloads
203 .iter()
204 .map(|(k, v)| (*k, v.clone())),
205 );
206 self
207 .completed_uploads
208 .extend(other.completed_uploads.iter().map(|(k, v)| (*k, v.clone())));
209 self
210 .running_downloads
211 .extend(other.running_downloads.iter().map(|(k, v)| (*k, v.clone())));
212 self
213 .running_uploads
214 .extend(other.running_uploads.iter().map(|(k, v)| (*k, v.clone())));
215 }
216
217 pub fn clear_derivation(
218 &mut self,
219 id: DerivationId,
220 old_status: &BuildStatus,
221 ) {
222 match old_status {
223 BuildStatus::Unknown => {},
224 BuildStatus::Planned => {
225 self.planned_builds.remove(&id);
226 },
227 BuildStatus::Building(_) => {
228 self.running_builds.remove(&id);
229 },
230 BuildStatus::Built { .. } => {
231 self.completed_builds.remove(&id);
232 },
233 BuildStatus::Failed { .. } => {
234 self.failed_builds.remove(&id);
235 },
236 }
237 }
238
239 pub fn update_derivation(
240 &mut self,
241 id: DerivationId,
242 new_status: &BuildStatus,
243 ) {
244 match new_status {
245 BuildStatus::Unknown => {},
246 BuildStatus::Planned => {
247 self.planned_builds.insert(id);
248 },
249 BuildStatus::Building(info) => {
250 self.running_builds.insert(id, info.clone());
251 },
252 BuildStatus::Built { info, end } => {
253 self.completed_builds.insert(id, CompletedBuildInfo {
254 start: info.start,
255 end: *end,
256 host: info.host.clone(),
257 });
258 },
259 BuildStatus::Failed { info, fail } => {
260 self.failed_builds.insert(id, FailedBuildInfo {
261 start: info.start,
262 end: fail.at,
263 host: info.host.clone(),
264 fail_type: fail.fail_type.clone(),
265 });
266 },
267 }
268 }
269}
270
271#[derive(Debug, Clone)]
273pub struct CompletedBuildInfo {
274 pub start: f64,
275 pub end: f64,
276 pub host: Host,
277}
278
279#[derive(Debug, Clone)]
281pub struct FailedBuildInfo {
282 pub start: f64,
283 pub end: f64,
284 pub host: Host,
285 pub fail_type: FailType,
286}
287
288#[derive(Debug, Clone)]
290pub struct ActivityStatus {
291 pub activity: u8,
292 pub text: String,
293 pub parent: Option<ActivityId>,
294 pub phase: Option<String>,
295 pub progress: Option<ActivityProgress>,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub struct ActivityProgress {
301 pub done: u64,
303 pub expected: u64,
305 pub running: u64,
307 pub failed: u64,
309}
310
311#[derive(Debug, Clone)]
313pub struct BuildReport {
314 pub derivation_name: String,
315 pub platform: String,
316 pub duration_secs: f64,
317 pub completed_at: SystemTime,
318 pub host: String,
319 pub success: bool,
320}
321
322#[derive(Debug, Clone, Default)]
324pub struct EvalInfo {
325 pub last_file_name: Option<String>,
326 pub count: usize,
327 pub at: f64,
328}
329
330#[derive(Debug, Clone)]
332pub struct State {
333 pub derivation_infos: IndexMap<DerivationId, DerivationInfo>,
334 pub store_path_infos: IndexMap<StorePathId, StorePathInfo>,
335 pub full_summary: DependencySummary,
336 pub forest_roots: Vec<DerivationId>,
337 pub build_cache: HashMap<(String, String), Vec<BuildReport>>,
338 pub start_time: f64,
339 pub progress_state: ProgressState,
340 pub store_path_ids: HashMap<StorePath, StorePathId>,
341 pub derivation_ids: HashMap<Derivation, DerivationId>,
342 pub touched_ids: HashSet<DerivationId>,
343 pub activities: HashMap<ActivityId, ActivityStatus>,
344 pub nix_errors: Vec<String>,
345 pub build_logs: Vec<String>,
346 pub traces: Vec<String>,
347 pub build_platform: Option<String>,
348 pub evaluation_state: EvalInfo,
349 pub builds_activity: Option<ActivityId>,
350 next_store_path_id: StorePathId,
351 next_derivation_id: DerivationId,
352}
353
354impl Default for State {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360impl State {
361 #[must_use]
362 pub fn new() -> Self {
363 Self {
364 derivation_infos: IndexMap::new(),
365 store_path_infos: IndexMap::new(),
366 full_summary: DependencySummary::default(),
367 forest_roots: Vec::new(),
368 build_cache: HashMap::new(),
369 start_time: current_time(),
370 progress_state: ProgressState::JustStarted,
371 store_path_ids: HashMap::new(),
372 derivation_ids: HashMap::new(),
373 touched_ids: HashSet::new(),
374 activities: HashMap::new(),
375 nix_errors: Vec::new(),
376 build_logs: Vec::new(),
377 traces: Vec::new(),
378 build_platform: None,
379 evaluation_state: EvalInfo::default(),
380 builds_activity: None,
381 next_store_path_id: 0,
382 next_derivation_id: 0,
383 }
384 }
385
386 #[must_use]
387 pub fn with_platform(platform: Option<String>) -> Self {
388 let mut state = Self::new();
389 state.build_platform = platform;
390 state
391 }
392
393 pub fn get_or_create_store_path_id(
394 &mut self,
395 path: StorePath,
396 ) -> StorePathId {
397 if let Some(&id) = self.store_path_ids.get(&path) {
398 return id;
399 }
400
401 let id = self.next_store_path_id;
402 self.next_store_path_id += 1;
403
404 self.store_path_infos.insert(id, StorePathInfo {
405 name: path.clone(),
406 producer: None,
407 input_for: HashSet::new(),
408 });
409 self.store_path_ids.insert(path, id);
410
411 id
412 }
413
414 pub fn get_or_create_derivation_id(
415 &mut self,
416 drv: Derivation,
417 ) -> DerivationId {
418 if let Some(&id) = self.derivation_ids.get(&drv) {
419 return id;
420 }
421
422 let id = self.next_derivation_id;
423 self.next_derivation_id += 1;
424
425 self.derivation_infos.insert(id, DerivationInfo {
426 name: drv.clone(),
427 outputs: HashMap::new(),
428 input_derivations: Vec::new(),
429 input_sources: HashSet::new(),
430 build_status: BuildStatus::Unknown,
431 dependency_summary: DependencySummary::default(),
432 cached: false,
433 derivation_parents: HashSet::new(),
434 pname: None,
435 platform: None,
436 });
437 self.derivation_ids.insert(drv, id);
438
439 id
440 }
441
442 pub fn populate_derivation_dependencies(&mut self, drv_id: DerivationId) {
444 use cognos::aterm;
445 use tracing::debug;
446
447 let already_parsed = self
450 .get_derivation_info(drv_id)
451 .map_or(false, |info| info.platform.is_some());
452
453 if already_parsed {
454 debug!("Skipping already-parsed derivation {}", drv_id);
455 return;
456 }
457
458 let drv_path = {
459 let info = match self.get_derivation_info(drv_id) {
460 Some(i) => i,
461 None => return,
462 };
463 info.name.path.display().to_string()
465 };
466
467 debug!("Attempting to parse .drv file: {}", drv_path);
468
469 let parsed = match aterm::parse_drv_file(&drv_path) {
470 Ok(p) => {
471 debug!(
472 "Successfully parsed .drv file: {} with {} input derivations",
473 drv_path,
474 p.input_drvs.len()
475 );
476 p
477 },
478 Err(e) => {
479 debug!("Failed to parse .drv file {}: {}", drv_path, e);
480 return;
481 },
482 };
483
484 if let Some(pname) = aterm::extract_pname(&parsed.env)
486 && let Some(info) = self.get_derivation_info_mut(drv_id)
487 {
488 info.pname = Some(pname);
489 }
490
491 if let Some(info) = self.get_derivation_info_mut(drv_id) {
492 info.platform = Some(parsed.platform);
493 }
494
495 for (output_name, store_path_str) in &parsed.outputs {
497 if let Some(sp) = StorePath::parse(store_path_str) {
498 let sp_id = self.get_or_create_store_path_id(sp);
499 if let Some(sp_info) = self.get_store_path_info_mut(sp_id) {
500 sp_info.producer = Some(drv_id);
501 }
502 if let Some(drv_info) = self.get_derivation_info_mut(drv_id) {
503 drv_info
504 .outputs
505 .insert(cognos::OutputName::parse(output_name), sp_id);
506 }
507 }
508 }
509
510 for (input_drv_path, outputs) in parsed.input_drvs {
512 if let Some(input_drv) = Derivation::parse(&input_drv_path) {
513 let input_drv_id = self.get_or_create_derivation_id(input_drv);
514
515 let mut output_set = HashSet::new();
526 for output in outputs {
527 output_set.insert(OutputName::parse(&output));
528 }
529
530 if let Some(parent_info) = self.get_derivation_info_mut(drv_id) {
532 let input = InputDerivation {
533 derivation: input_drv_id,
534 outputs: output_set,
535 };
536 if parent_info
537 .input_derivations
538 .iter()
539 .any(|d| d.derivation == input_drv_id)
540 {
541 debug!(
542 "Input derivation {} already in parent {}",
543 input_drv_id, drv_id
544 );
545 } else {
546 parent_info.input_derivations.push(input);
547 debug!(
548 "Added input derivation {} to {} (parent now has {} inputs)",
549 input_drv_id,
550 drv_id,
551 parent_info.input_derivations.len()
552 );
553 }
554 } else {
555 debug!(
556 "Parent derivation {} not found when trying to add input {}",
557 drv_id, input_drv_id
558 );
559 }
560
561 if let Some(child_info) = self.get_derivation_info_mut(input_drv_id) {
563 child_info.derivation_parents.insert(drv_id);
564 }
565
566 self.forest_roots.retain(|&id| id != input_drv_id);
568
569 }
572 }
573 }
574
575 #[must_use]
576 pub fn get_derivation_info(
577 &self,
578 id: DerivationId,
579 ) -> Option<&DerivationInfo> {
580 self.derivation_infos.get(&id)
581 }
582
583 pub fn get_derivation_info_mut(
584 &mut self,
585 id: DerivationId,
586 ) -> Option<&mut DerivationInfo> {
587 self.derivation_infos.get_mut(&id)
588 }
589
590 #[must_use]
591 pub fn get_store_path_info(&self, id: StorePathId) -> Option<&StorePathInfo> {
592 self.store_path_infos.get(&id)
593 }
594
595 pub fn get_store_path_info_mut(
596 &mut self,
597 id: StorePathId,
598 ) -> Option<&mut StorePathInfo> {
599 self.store_path_infos.get_mut(&id)
600 }
601
602 pub fn update_build_status(
603 &mut self,
604 id: DerivationId,
605 new_status: BuildStatus,
606 ) {
607 if let Some(info) = self.derivation_infos.get_mut(&id) {
608 let old_status =
609 std::mem::replace(&mut info.build_status, new_status.clone());
610 self.full_summary.clear_derivation(id, &old_status);
611 self.full_summary.update_derivation(id, &new_status);
612 self.touched_ids.insert(id);
613 }
614
615 self.propagate_to_parents(id);
617 }
618
619 fn recompute_own_summary(&mut self, id: DerivationId) {
623 let info = match self.derivation_infos.get(&id) {
624 Some(info) => info,
625 None => return,
626 };
627
628 let mut summary = DependencySummary::default();
629 summary.update_derivation(id, &info.build_status);
630
631 if let Some(info_mut) = self.derivation_infos.get_mut(&id) {
632 info_mut.dependency_summary = summary;
633 }
634 }
635
636 fn recompute_derivation_summary(&mut self, id: DerivationId) {
640 self.recompute_own_summary(id);
642
643 let children_ids: Vec<DerivationId> = {
645 let info = match self.derivation_infos.get(&id) {
646 Some(info) => info,
647 None => return,
648 };
649 info
650 .input_derivations
651 .iter()
652 .map(|input| input.derivation)
653 .collect()
654 };
655
656 let mut merged = DependencySummary::default();
657 if let Some(info) = self.derivation_infos.get(&id) {
659 merged.merge(&info.dependency_summary);
660 }
661 for child_id in children_ids {
663 if let Some(child_info) = self.derivation_infos.get(&child_id) {
664 merged.merge(&child_info.dependency_summary);
665 }
666 }
667
668 if let Some(info_mut) = self.derivation_infos.get_mut(&id) {
669 info_mut.dependency_summary = merged;
670 }
671 }
672
673 fn propagate_to_parents(&mut self, id: DerivationId) {
676 let mut ancestors: Vec<DerivationId> = Vec::new();
678 let mut current_parents = self.derivation_parents(id);
679 let mut visited: HashSet<DerivationId> = HashSet::new();
680
681 while let Some(parent_id) = current_parents.pop() {
682 if visited.insert(parent_id) {
683 ancestors.push(parent_id);
684 for grandparent_id in self.derivation_parents(parent_id) {
686 current_parents.push(grandparent_id);
687 }
688 }
689 }
690
691 for ancestor_id in ancestors.into_iter().rev() {
695 self.recompute_derivation_summary(ancestor_id);
696 }
697 }
698
699 fn derivation_parents(&self, id: DerivationId) -> Vec<DerivationId> {
701 let info = match self.derivation_infos.get(&id) {
702 Some(info) => info,
703 None => return Vec::new(),
704 };
705 info.derivation_parents.iter().copied().collect()
706 }
707
708 #[must_use]
709 pub fn has_errors(&self) -> bool {
710 !self.nix_errors.is_empty() || !self.full_summary.failed_builds.is_empty()
711 }
712
713 #[must_use]
714 pub fn total_builds(&self) -> usize {
715 self.full_summary.planned_builds.len()
716 + self.full_summary.running_builds.len()
717 + self.full_summary.completed_builds.len()
718 + self.full_summary.failed_builds.len()
719 }
720
721 #[must_use]
722 pub fn running_builds_for_host(
723 &self,
724 host: &Host,
725 ) -> Vec<(DerivationId, &BuildInfo)> {
726 self
727 .full_summary
728 .running_builds
729 .iter()
730 .filter(|(_, info)| &info.host == host)
731 .map(|(id, info)| (*id, info))
732 .collect()
733 }
734
735 #[must_use]
737 pub fn has_platform_mismatch(&self, id: DerivationId) -> bool {
738 if let (Some(build_platform), Some(info)) =
739 (&self.build_platform, self.get_derivation_info(id))
740 && let Some(drv_platform) = &info.platform
741 {
742 return build_platform != drv_platform;
743 }
744 false
745 }
746
747 #[must_use]
749 pub fn platform_mismatches(&self) -> Vec<DerivationId> {
750 self
751 .derivation_infos
752 .keys()
753 .filter(|&&id| self.has_platform_mismatch(id))
754 .copied()
755 .collect()
756 }
757
758 #[must_use]
765 pub fn get_activity_prefix(
766 &self,
767 activity_id: ActivityId,
768 prefix_style: &crate::types::LogPrefixStyle,
769 use_color: bool,
770 ) -> Option<String> {
771 use cognos::Activities;
772
773 use crate::types::LogPrefixStyle;
774
775 if matches!(prefix_style, LogPrefixStyle::None) {
777 return Some(String::new());
778 }
779
780 let mut current_id = activity_id;
781 let max_depth = 10; let mut depth = 0;
783
784 while depth < max_depth {
785 if let Some(activity) = self.activities.get(¤t_id) {
786 if activity.activity == Activities::Build as u8 {
788 if let Some(drv) = extract_derivation_from_text(&activity.text) {
792 let drv_id = self.derivation_ids.get(&drv);
794 let name = if matches!(prefix_style, LogPrefixStyle::Short) {
795 if let Some(id) = drv_id {
797 if let Some(drv_info) = self.derivation_infos.get(id) {
798 if let Some(pname) = &drv_info.pname {
799 pname.clone()
800 } else {
801 drv.name.clone()
802 }
803 } else {
804 drv.name.clone()
805 }
806 } else {
807 drv.name.clone()
808 }
809 } else {
810 drv.name.clone()
812 };
813
814 let colored_name = if use_color
816 && std::io::IsTerminal::is_terminal(&std::io::stderr())
817 {
818 format!("\x1b[34m{name}\x1b[0m")
819 } else {
820 name
821 };
822
823 return Some(format!("{colored_name}> "));
824 }
825 }
826
827 if let Some(parent_id) = activity.parent {
829 if parent_id == 0 {
830 break; }
832 current_id = parent_id;
833 depth += 1;
834 } else {
835 break;
836 }
837 } else {
838 break;
839 }
840 }
841
842 None
843 }
844}
845
846fn extract_derivation_from_text(text: &str) -> Option<Derivation> {
849 if let Some(start) = text.find("/nix/store/")
851 && let Some(end) = text[start..].find(".drv")
852 {
853 let drv_path = &text[start..start + end + 4]; return Derivation::parse(drv_path);
855 }
856 None
857}
858
859#[must_use]
860pub fn current_time() -> f64 {
861 SystemTime::now()
862 .duration_since(SystemTime::UNIX_EPOCH)
863 .unwrap_or(Duration::ZERO)
864 .as_secs_f64()
865}