Skip to main content

rom_core/
state.rs

1//! State management for ROM
2use 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
12/// Unique identifier for store paths
13pub type StorePathId = usize;
14
15/// Unique identifier for derivations
16pub type DerivationId = usize;
17
18/// Unique identifier for activities
19pub type ActivityId = Id;
20
21/// Store path representation
22#[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/// Derivation representation
53#[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/// Transfer information (download/upload)
85#[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/// Completed transfer information
95#[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/// Store path information
104#[derive(Debug, Clone)]
105pub struct StorePathInfo {
106  pub name:      StorePath,
107  pub producer:  Option<DerivationId>,
108  pub input_for: HashSet<DerivationId>,
109}
110
111/// Build information
112#[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/// Build failure information
121#[derive(Debug, Clone)]
122pub struct BuildFail {
123  pub at:        f64,
124  pub fail_type: FailType,
125}
126
127/// Failure type
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum FailType {
130  BuildFailed(i32),
131  Timeout,
132  HashMismatch,
133  DependencyFailed,
134  Unknown,
135}
136
137/// Build status
138#[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/// Input derivation for dependency tracking
148#[derive(Debug, Clone)]
149pub struct InputDerivation {
150  pub derivation: DerivationId,
151  pub outputs:    HashSet<OutputName>,
152}
153
154/// Derivation information
155#[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/// Dependency summary for tracking build progress
170#[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/// Completed build information
272#[derive(Debug, Clone)]
273pub struct CompletedBuildInfo {
274  pub start: f64,
275  pub end:   f64,
276  pub host:  Host,
277}
278
279/// Failed build information
280#[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/// Activity status tracking
289#[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/// Activity progress for downloads/uploads/builds
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub struct ActivityProgress {
301  /// Bytes completed
302  pub done:     u64,
303  /// Total bytes expected
304  pub expected: u64,
305  /// Currently running transfers
306  pub running:  u64,
307  /// Failed transfers
308  pub failed:   u64,
309}
310
311/// Build report for caching
312#[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/// Evaluation information
323#[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/// Main state for ROM
331#[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  /// Populate derivation dependencies by parsing its .drv file
443  pub fn populate_derivation_dependencies(&mut self, drv_id: DerivationId) {
444    use cognos::aterm;
445    use tracing::debug;
446
447    // platform is always set after a successful parse; use it as the
448    // "already parsed" marker so leaf nodes (zero inputs) are not re-parsed.
449    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      // Path already includes .drv extension from Derivation::parse
464      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    // Extract metadata
485    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    // Register the derivation's output store paths
496    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    // Process input derivations
511    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        // Do NOT auto-mark inputs as Planned here.  A derivation should only
516        // be marked Planned when Nix explicitly reports it will be built (via
517        // a build-queued or similar protocol event).  Inputs that are already
518        // in the store are Unknown and have an empty dependencySummary; the
519        // tree renderer filters them out (node_is_visible returns false for
520        // Unknown nodes with an empty summary).  Marking them Planned here
521        // causes all cached/already-built inputs to incorrectly appear in the
522        // tree, which is exactly the discrepancy with NOM's output.
523
524        // Create output set
525        let mut output_set = HashSet::new();
526        for output in outputs {
527          output_set.insert(OutputName::parse(&output));
528        }
529
530        // Add to parent's input derivations
531        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        // Mark child as having this parent
562        if let Some(child_info) = self.get_derivation_info_mut(input_drv_id) {
563          child_info.derivation_parents.insert(drv_id);
564        }
565
566        // Remove from forest roots if it has a parent
567        self.forest_roots.retain(|&id| id != input_drv_id);
568
569        // Do not recurse: child dependencies are populated lazily when nix
570        // reports starting those builds via JSON events.
571      }
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    // Propagate changes up the parent chain
616    self.propagate_to_parents(id);
617  }
618
619  /// Recompute a derivation's own dependency_summary based on its build_status.
620  /// This does NOT include children's summaries. That's done by
621  /// `propagate_to_parents`.
622  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  /// Recompute a derivation's full dependency_summary by merging:
637  /// 1. Its own contribution (based on build_status)
638  /// 2. All its children's dependency_summaries
639  fn recompute_derivation_summary(&mut self, id: DerivationId) {
640    // First, compute our own contribution
641    self.recompute_own_summary(id);
642
643    // Then merge all children's summaries
644    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    // Our own summary
658    if let Some(info) = self.derivation_infos.get(&id) {
659      merged.merge(&info.dependency_summary);
660    }
661    // Merge children's summaries
662    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  /// Propagate a status change up the parent chain by recomputing each
674  /// ancestor's dependency_summary. This is for O(1) subtree aggregation.
675  fn propagate_to_parents(&mut self, id: DerivationId) {
676    // Collect all ancestors first to avoid borrowing issues
677    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        // Get this parent's parents for the next iteration
685        for grandparent_id in self.derivation_parents(parent_id) {
686          current_parents.push(grandparent_id);
687        }
688      }
689    }
690
691    // Recompute summaries from leaves up (reverse order of discovery)
692    // Since we collected ancestors in BFS order, we need to process them
693    // from end to beginning to go bottom-up
694    for ancestor_id in ancestors.into_iter().rev() {
695      self.recompute_derivation_summary(ancestor_id);
696    }
697  }
698
699  /// Get the parent derivations (derivations that depend on this one)
700  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  /// Check if a derivation has a platform mismatch
736  #[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  /// Get all derivations with platform mismatches
748  #[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  /// Get the activity prefix for a given activity ID by walking up the parent
759  /// chain to find a Build activity and extracting its derivation name.
760  /// Returns a prefix like "hello> " suitable for prepending to log lines.
761  /// If `use_color` is true and stderr is a TTY, the prefix will be blue.
762  /// The `prefix_style` determines whether to use short (pname only), full, or
763  /// no prefix.
764  #[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 prefix style is None, return empty string
776    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; // Prevent infinite loops
782    let mut depth = 0;
783
784    while depth < max_depth {
785      if let Some(activity) = self.activities.get(&current_id) {
786        // Check if this is a Build activity (type 105)
787        if activity.activity == Activities::Build as u8 {
788          // Extract derivation path from the text field
789          // The text field typically contains something like:
790          // "building '/nix/store/...-hello-2.10.drv'"
791          if let Some(drv) = extract_derivation_from_text(&activity.text) {
792            // Look up the DerivationInfo for this derivation
793            let drv_id = self.derivation_ids.get(&drv);
794            let name = if matches!(prefix_style, LogPrefixStyle::Short) {
795              // Try to use pname if available
796              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              // Full style - use full derivation name
811              drv.name.clone()
812            };
813
814            // Apply color if requested and stderr is a TTY
815            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        // Move to parent activity
828        if let Some(parent_id) = activity.parent {
829          if parent_id == 0 {
830            break; // Reached root
831          }
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
846/// Extract derivation from activity text like "building
847/// '/nix/store/...-hello-2.10.drv'" Returns the Derivation object
848fn extract_derivation_from_text(text: &str) -> Option<Derivation> {
849  // Look for .drv path in text
850  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]; // Include .drv
854    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}