Skip to main content

rom_core/
update.rs

1//! State update logic for processing nix messages
2use cognos::{
3  Actions,
4  Activities,
5  Host,
6  Id,
7  ProgressState,
8  ResultType,
9  Verbosity,
10};
11use tracing::{debug, trace};
12
13use crate::{
14  cache::BuildReportCache,
15  state::{
16    ActivityProgress,
17    ActivityStatus,
18    BuildFail,
19    BuildInfo,
20    BuildReport,
21    BuildStatus,
22    CompletedTransferInfo,
23    Derivation,
24    DerivationId,
25    FailType,
26    InputDerivation,
27    State,
28    StorePath,
29    StorePathId,
30    TransferInfo,
31    current_time,
32  },
33};
34
35/// Process a nix JSON message and update state
36pub fn process_message(state: &mut State, action: Actions) -> bool {
37  let now = current_time();
38  let mut changed = false;
39
40  // Mark that we've received input
41  if state.progress_state == ProgressState::JustStarted {
42    state.progress_state = ProgressState::InputReceived;
43    changed = true;
44  }
45
46  trace!("Processing action: {:?}", action);
47
48  match action {
49    Actions::Start {
50      id,
51      level,
52      parent,
53      text,
54      activity,
55      fields,
56    } => {
57      changed |=
58        handle_start(state, id, level, parent, text, activity, fields, now);
59    },
60    Actions::Stop { id } => {
61      changed |= handle_stop(state, id, now);
62    },
63    Actions::Message {
64      level,
65      msg,
66      raw_msg,
67      ..
68    } => {
69      // Prefer raw_msg (Lix). It's the message without ANSI escape codes.
70      // Fall back to msg for Nix, which doesn't provide raw_msg.
71      let clean = raw_msg.unwrap_or(msg);
72      changed |= handle_message(state, level, clean);
73    },
74    Actions::Result {
75      id,
76      result_type,
77      fields,
78    } => {
79      changed |= handle_result(state, id, result_type, fields, now);
80    },
81  }
82
83  changed
84}
85
86fn handle_start(
87  state: &mut State,
88  id: Id,
89  _level: Verbosity,
90  parent: Id,
91  text: String,
92  activity: Activities,
93  fields: Vec<serde_json::Value>,
94  now: f64,
95) -> bool {
96  // Store activity status
97  let parent_id = if parent == 0 { None } else { Some(parent) };
98
99  let activity_u8 = activity as u8;
100
101  state.activities.insert(id, ActivityStatus {
102    activity: activity_u8,
103    text:     text.clone(),
104    parent:   parent_id,
105    phase:    None,
106    progress: None,
107  });
108
109  let changed = match activity_u8 {
110    105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Build */
111    108 => handle_substitute_start(state, id, &text, &fields, now), /* Substitute */
112    109 => handle_query_path_info_start(state, id, &text, &fields, now), /* QueryPathInfo */
113    110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */
114    101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */
115    100 => handle_copy_path_start(state, id, &text, &fields, now), /* CopyPath */
116    104 => {
117      // Builds activity - track this as the top-level builds activity
118      if state.builds_activity.is_none() {
119        state.builds_activity = Some(id);
120        true
121      } else {
122        false
123      }
124    },
125    102 | 103 | 106 | 107 | 111 | 112 => {
126      // Realise, CopyPaths, OptimiseStore, VerifyPaths, BuildWaiting, FetchTree
127      // These activities have no fields and are just tracked
128      true
129    },
130    _ => {
131      debug!("Unknown activity type: {}", activity_u8);
132      false
133    },
134  };
135
136  // Track parent-child relationships for dependency tree
137  if changed && activity_u8 == 105 && parent_id.is_some() {
138    let parent_act_id = parent_id.unwrap();
139
140    // Find parent and child derivation IDs
141    let parent_drv_id = find_derivation_by_activity(state, parent_act_id);
142    let child_drv_id = find_derivation_by_activity(state, id);
143
144    if let Some(parent_drv_id) = parent_drv_id
145      && let Some(child_drv_id) = child_drv_id
146    {
147      debug!(
148        "Establishing parent-child relationship: parent={parent_drv_id}, \
149         child={child_drv_id}"
150      );
151
152      // Add child as a dependency of parent
153      if let Some(parent_info) = state.get_derivation_info_mut(parent_drv_id) {
154        let input = InputDerivation {
155          derivation: child_drv_id,
156          outputs:    std::collections::HashSet::new(),
157        };
158        if !parent_info
159          .input_derivations
160          .iter()
161          .any(|d| d.derivation == child_drv_id)
162        {
163          parent_info.input_derivations.push(input);
164          debug!("Added child to parent's input_derivations");
165        }
166      }
167      // Mark child as having a parent
168      if let Some(child_info) = state.get_derivation_info_mut(child_drv_id) {
169        child_info.derivation_parents.insert(parent_drv_id);
170      }
171      // Remove child from forest roots since it has a parent
172      state.forest_roots.retain(|&id| id != child_drv_id);
173    }
174  }
175
176  changed
177}
178
179fn handle_stop(state: &mut State, id: Id, now: f64) -> bool {
180  let activity = state.activities.get(&id).cloned();
181
182  if let Some(activity_status) = activity {
183    state.activities.remove(&id);
184
185    match activity_status.activity {
186      105 => handle_build_stop(state, id, now), // Build
187      108 => handle_substitute_stop(state, id, now), // Substitute
188      101 | 100 => handle_transfer_stop(state, id, now), // FileTransfer,
189      // CopyPath
190      109 | 110 => {
191        // QueryPathInfo, PostBuildHook - just acknowledge stop
192        false
193      },
194      102 | 103 | 104 | 106 | 107 | 111 | 112 => {
195        // Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting,
196        // FetchTree
197        false
198      },
199      _ => false,
200    }
201  } else {
202    false
203  }
204}
205
206fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool {
207  // Store all build logs for display
208  state.build_logs.push(msg.clone());
209
210  // Extract phase from log messages like "Running phase: configurePhase"
211  if let Some(phase_start) = msg.find("Running phase: ") {
212    let phase_name = &msg[phase_start + 15..]; // skip "Running phase: "
213    let phase = phase_name.trim().to_string();
214
215    // Find the active build and update its phase
216    for activity in state.activities.values_mut() {
217      if activity.activity == 105 {
218        // Build activity
219        activity.phase = Some(phase.clone());
220      }
221    }
222  }
223
224  match level {
225    Verbosity::Error => {
226      // Track errors
227      if msg.contains("error:") || msg.contains("failed") {
228        state.nix_errors.push(msg.clone());
229
230        // Try to extract which build failed
231        if let Some(drv_path) = extract_derivation_from_error(&msg)
232          && let Some(drv) = Derivation::parse(&drv_path)
233        {
234          let drv_id = state.get_or_create_derivation_id(drv);
235
236          // Get build info first
237          let build_info_opt =
238            state.get_derivation_info(drv_id).and_then(|info| {
239              if let BuildStatus::Building(build_info) = &info.build_status {
240                Some(build_info.clone())
241              } else {
242                None
243              }
244            });
245
246          if let Some(build_info) = build_info_opt {
247            let fail = BuildFail {
248              at:        current_time(),
249              fail_type: parse_fail_type(&msg),
250            };
251
252            state.update_build_status(drv_id, BuildStatus::Failed {
253              info: build_info,
254              fail,
255            });
256          }
257        }
258        return true;
259      }
260      false
261    },
262    Verbosity::Info | Verbosity::Notice => {
263      // Track info messages for evaluation progress
264      if msg.contains("evaluating") || msg.contains("copying") {
265        // Update evaluation state
266        if let Some(file_name) = extract_file_name(&msg) {
267          state.evaluation_state.last_file_name = Some(file_name);
268          state.evaluation_state.count += 1;
269          state.evaluation_state.at = current_time();
270        }
271      }
272      true // return true since we stored the log
273    },
274    Verbosity::Talkative
275    | Verbosity::Chatty
276    | Verbosity::Debug
277    | Verbosity::Vomit => {
278      // These are trace-level messages, store separately
279      state.traces.push(msg);
280      true
281    },
282    _ => {
283      true // return true since we stored the log
284    },
285  }
286}
287
288fn handle_result(
289  state: &mut State,
290  id: Id,
291  result_type: ResultType,
292  fields: Vec<serde_json::Value>,
293  _now: f64,
294) -> bool {
295  match result_type {
296    ResultType::FileLinked => {
297      if fields.len() >= 2 {
298        debug!(
299          "FileLinked: {}/{}",
300          fields[0].as_u64().unwrap_or(0),
301          fields[1].as_u64().unwrap_or(0)
302        );
303      }
304      false
305    },
306    ResultType::BuildLogLine => {
307      if let Some(line) = fields.first().and_then(|f| f.as_str()) {
308        state.build_logs.push(line.to_string());
309        return true;
310      }
311      false
312    },
313    ResultType::UntrustedPath => {
314      if let Some(path) = fields.first().and_then(|f| f.as_str()) {
315        debug!("Untrusted path: {}", path);
316        state.nix_errors.push(format!("Untrusted path: {path}"));
317        return true;
318      }
319      false
320    },
321    ResultType::CorruptedPath => {
322      if let Some(path) = fields.first().and_then(|f| f.as_str()) {
323        state.nix_errors.push(format!("Corrupted path: {path}"));
324        return true;
325      }
326      false
327    },
328    ResultType::SetPhase => {
329      if let Some(phase) = fields.first().and_then(|f| f.as_str())
330        && let Some(activity) = state.activities.get_mut(&id)
331      {
332        activity.phase = Some(phase.to_string());
333        return true;
334      }
335      false
336    },
337    ResultType::Progress => {
338      if fields.len() >= 4
339        && let (Some(done), Some(expected), Some(running), Some(failed)) = (
340          fields[0].as_u64(),
341          fields[1].as_u64(),
342          fields[2].as_u64(),
343          fields[3].as_u64(),
344        )
345        && let Some(activity) = state.activities.get_mut(&id)
346      {
347        activity.progress = Some(ActivityProgress {
348          done,
349          expected,
350          running,
351          failed,
352        });
353        return true;
354      }
355      false
356    },
357    ResultType::SetExpected => {
358      if fields.len() >= 2 {
359        debug!(
360          "SetExpected: activity_type={}, count={}",
361          fields[0].as_u64().unwrap_or(0),
362          fields[1].as_u64().unwrap_or(0)
363        );
364      }
365      false
366    },
367    ResultType::PostBuildLogLine => {
368      if let Some(line) = fields.first().and_then(|f| f.as_str()) {
369        state.build_logs.push(format!("[post-build] {line}"));
370        return true;
371      }
372      false
373    },
374    ResultType::FetchStatus => {
375      if let Some(status) = fields.first().and_then(|f| f.as_str()) {
376        debug!("Fetch status: {status}");
377      }
378      false
379    },
380  }
381}
382
383/// Get build time estimate from cache
384fn get_build_estimate(
385  state: &State,
386  derivation_name: &str,
387  host: &Host,
388) -> Option<u64> {
389  // Use pname if available, otherwise derivation name
390  let lookup_name = derivation_name.to_string();
391  let host_str = host.name();
392
393  BuildReportCache::calculate_median(
394    state
395      .build_cache
396      .get(&(host_str.to_string(), lookup_name))?
397      .as_slice(),
398  )
399}
400
401/// Record completed build for future predictions
402fn record_build_completion(
403  state: &mut State,
404  derivation_name: String,
405  platform: Option<String>,
406  start: f64,
407  end: f64,
408  host: &Host,
409) {
410  let duration_secs = end - start;
411  let completed_at = std::time::SystemTime::now();
412
413  let report = BuildReport {
414    derivation_name: derivation_name.clone(),
415    platform: platform.unwrap_or_default(),
416    duration_secs,
417    completed_at,
418    host: host.name().to_string(),
419    success: true,
420  };
421
422  // Store in state for later CSV persistence
423  let key = (host.name().to_string(), derivation_name);
424  state.build_cache.entry(key).or_default().push(report);
425}
426
427fn handle_build_start(
428  state: &mut State,
429  id: Id,
430  parent_id: Option<Id>,
431  text: &str,
432  fields: &[serde_json::Value],
433  now: f64,
434) -> bool {
435  debug!(
436    "handle_build_start: id={}, text={}, fields={:?}",
437    id, text, fields
438  );
439
440  // First try to get derivation path from fields
441  let drv_path = if fields.is_empty() {
442    extract_derivation_path(text)
443  } else {
444    fields[0].as_str().map(std::string::ToString::to_string)
445  };
446
447  if let Some(drv_path) = drv_path {
448    debug!("Extracted derivation path: {}", drv_path);
449    if let Some(drv) = Derivation::parse(&drv_path) {
450      let drv_id = state.get_or_create_derivation_id(drv.clone());
451      let host =
452        parse_host(fields.get(1).and_then(|v| v.as_str()).unwrap_or(""));
453
454      // Get build time estimate from cache
455      let estimate = get_build_estimate(state, &drv.name, &host);
456
457      let build_info = BuildInfo {
458        start: now,
459        host,
460        estimate,
461        activity_id: Some(id),
462      };
463
464      debug!("Setting derivation {} to Building status", drv_id);
465      state.update_build_status(drv_id, BuildStatus::Building(build_info));
466      debug!(
467        "After update_build_status, state has {} derivations",
468        state.derivation_infos.len()
469      );
470
471      // Parse .drv file to populate dependency tree
472      state.populate_derivation_dependencies(drv_id);
473      debug!(
474        "After populate_derivation_dependencies, state has {} derivations",
475        state.derivation_infos.len()
476      );
477
478      // Mark as forest root if no parent
479      if parent_id.is_none() && !state.forest_roots.contains(&drv_id) {
480        state.forest_roots.push(drv_id);
481      }
482
483      return true;
484    }
485    debug!("Failed to parse derivation from path: {}", drv_path);
486  } else {
487    debug!(
488      "No derivation path in fields for Build activity {} - this should not \
489       happen",
490      id
491    );
492  }
493  false
494}
495
496fn handle_build_stop(state: &mut State, id: Id, now: f64) -> bool {
497  // Find the derivation associated with this Build activity. Per NOM's design,
498  // Stop for a Build activity means the build completed.
499  let result = state.derivation_infos.iter().find_map(|(drv_id, info)| {
500    if let BuildStatus::Building(build_info) = &info.build_status {
501      if build_info.activity_id == Some(id) {
502        Some((
503          *drv_id,
504          build_info.clone(),
505          info.name.name.clone(),
506          info.platform.clone(),
507        ))
508      } else {
509        None
510      }
511    } else {
512      None
513    }
514  });
515
516  if let Some((drv_id, build_info, name, platform)) = result {
517    let start = build_info.start;
518    let host = build_info.host.clone();
519    state.update_build_status(drv_id, BuildStatus::Built {
520      info: build_info,
521      end:  now,
522    });
523    record_build_completion(state, name, platform, start, now, &host);
524    debug!("Build completed for derivation {drv_id}");
525    return true;
526  }
527
528  debug!(
529    "Build stopped for activity {id} but no matching building derivation found"
530  );
531  false
532}
533
534fn handle_substitute_start(
535  state: &mut State,
536  id: Id,
537  text: &str,
538  fields: &[serde_json::Value],
539  now: f64,
540) -> bool {
541  // Extract store path
542  let path_str = if fields.is_empty() {
543    extract_store_path(text)
544  } else {
545    fields[0].as_str().map(std::string::ToString::to_string)
546  };
547
548  if let Some(path_str) = path_str
549    && let Some(path) = StorePath::parse(&path_str)
550  {
551    let path_id = state.get_or_create_store_path_id(path);
552    let host = parse_host(fields.get(1).and_then(|v| v.as_str()).unwrap_or(""));
553
554    let transfer = TransferInfo {
555      start: now,
556      host,
557      activity_id: id,
558      bytes_transferred: 0,
559      total_bytes: None,
560    };
561
562    state
563      .full_summary
564      .running_downloads
565      .insert(path_id, transfer);
566
567    return true;
568  }
569  false
570}
571
572fn handle_substitute_stop(state: &mut State, id: Id, now: f64) -> bool {
573  // Find the store path associated with this activity
574  let result = state.full_summary.running_downloads.iter().find_map(
575    |(path_id, transfer_info)| {
576      if transfer_info.activity_id == id {
577        Some((*path_id, transfer_info.clone()))
578      } else {
579        None
580      }
581    },
582  );
583
584  if let Some((path_id, transfer_info)) = result {
585    state.full_summary.running_downloads.remove(&path_id);
586    state.full_summary.completed_downloads.insert(
587      path_id,
588      CompletedTransferInfo {
589        start:       transfer_info.start,
590        end:         now,
591        host:        transfer_info.host,
592        total_bytes: transfer_info.bytes_transferred,
593      },
594    );
595    return true;
596  }
597
598  false
599}
600
601fn handle_file_transfer_start(
602  _state: &mut State,
603  id: Id,
604  _text: &str,
605  fields: &[serde_json::Value],
606  _now: f64,
607) -> bool {
608  // FileTransfer expects 1 text field: URL or description
609  if fields.is_empty() {
610    debug!("FileTransfer activity {} has no fields", id);
611    return false;
612  }
613
614  // Just track the activity, actual progress comes via Result messages
615  true
616}
617
618fn handle_copy_path_start(
619  state: &mut State,
620  id: Id,
621  _text: &str,
622  fields: &[serde_json::Value],
623  now: f64,
624) -> bool {
625  // CopyPath expects 3 text fields: path, from, to
626  if fields.len() < 3 {
627    debug!("CopyPath activity {} has insufficient fields", id);
628    return false;
629  }
630
631  let path_str = fields[0].as_str();
632  let _from_host = fields[1].as_str().map(|s| {
633    if s.is_empty() || s == "localhost" {
634      Host::Localhost
635    } else {
636      Host::Remote(s.to_string())
637    }
638  });
639  let to_host = fields[2].as_str().map(|s| {
640    if s.is_empty() || s == "localhost" {
641      Host::Localhost
642    } else {
643      Host::Remote(s.to_string())
644    }
645  });
646
647  if let (Some(path_str), Some(to)) = (path_str, to_host)
648    && let Some(path) = StorePath::parse(path_str)
649  {
650    let path_id = state.get_or_create_store_path_id(path);
651
652    let transfer = TransferInfo {
653      start:             now,
654      host:              to, // destination host
655      activity_id:       id,
656      bytes_transferred: 0,
657      total_bytes:       None,
658    };
659
660    // CopyPath is an upload from 'from' to 'to'
661    state.full_summary.running_uploads.insert(path_id, transfer);
662    return true;
663  }
664
665  false
666}
667
668fn handle_query_path_info_start(
669  _state: &mut State,
670  id: Id,
671  _text: &str,
672  fields: &[serde_json::Value],
673  _now: f64,
674) -> bool {
675  // QueryPathInfo expects 2 text fields: path, host
676  if fields.len() < 2 {
677    debug!("QueryPathInfo activity {} has insufficient fields", id);
678    return false;
679  }
680
681  // Just track the activity
682  true
683}
684
685fn handle_post_build_hook_start(
686  _state: &mut State,
687  id: Id,
688  _text: &str,
689  fields: &[serde_json::Value],
690  _now: f64,
691) -> bool {
692  // PostBuildHook expects 1 text field: derivation path
693  if fields.is_empty() {
694    debug!("PostBuildHook activity {} has no fields", id);
695    return false;
696  }
697
698  let drv_path = fields[0].as_str();
699  if let Some(drv_path) = drv_path
700    && let Some(_drv) = Derivation::parse(drv_path)
701  {
702    // Just track that the hook is running
703    return true;
704  }
705
706  false
707}
708
709fn handle_transfer_stop(state: &mut State, id: Id, now: f64) -> bool {
710  // Check downloads; find the matching path_id without cloning the entire map
711  if let Some(path_id) = state
712    .full_summary
713    .running_downloads
714    .iter()
715    .find(|(_, transfer_info)| transfer_info.activity_id == id)
716    .map(|(&id, _)| id)
717  {
718    let transfer_info = state.full_summary.running_downloads.remove(&path_id);
719
720    if let Some(transfer_info) = transfer_info {
721      let completed = CompletedTransferInfo {
722        start:       transfer_info.start,
723        end:         now,
724        host:        transfer_info.host.clone(),
725        total_bytes: transfer_info.bytes_transferred,
726      };
727
728      state
729        .full_summary
730        .completed_downloads
731        .insert(path_id, completed);
732    }
733    return true;
734  }
735
736  // Check uploads
737  if let Some(path_id) = state
738    .full_summary
739    .running_uploads
740    .iter()
741    .find(|(_, transfer_info)| transfer_info.activity_id == id)
742    .map(|(&id, _)| id)
743  {
744    let transfer_info = state.full_summary.running_uploads.remove(&path_id);
745
746    if let Some(transfer_info) = transfer_info {
747      let completed = CompletedTransferInfo {
748        start:       transfer_info.start,
749        end:         now,
750        host:        transfer_info.host.clone(),
751        total_bytes: transfer_info.bytes_transferred,
752      };
753
754      state
755        .full_summary
756        .completed_uploads
757        .insert(path_id, completed);
758    }
759    return true;
760  }
761
762  false
763}
764
765fn extract_derivation_path(text: &str) -> Option<String> {
766  // Look for .drv paths in the text
767  if let Some(start) = text.find("/nix/store/")
768    && let Some(end) = text[start..].find(".drv")
769  {
770    return Some(text[start..start + end + 4].to_string());
771  }
772  None
773}
774
775fn extract_store_path(text: &str) -> Option<String> {
776  // Look for store paths in the text
777  if let Some(start) = text.find("/nix/store/") {
778    // Find the end of the path (space or end of string)
779    let rest = &text[start..];
780    let end = rest
781      .find(|c: char| c.is_whitespace() || c == '\'' || c == '"')
782      .unwrap_or(rest.len());
783    return Some(rest[..end].to_string());
784  }
785  None
786}
787
788/// Parse a host from the fields[1] string of a Build or Substitute activity.
789///
790/// - Empty string, "local", "local://", "unix", "unix://" -> Localhost
791/// - Properly strips proto:// prefix and extracts just the hostname
792/// - Strips user@ from user@host format
793fn parse_host(s: &str) -> Host {
794  let s = s.trim();
795
796  // Handle known localhost aliases
797  if s.is_empty()
798    || s == "localhost"
799    || s == "local"
800    || s == "local://"
801    || s == "unix"
802    || s == "unix://"
803  {
804    return Host::Localhost;
805  }
806
807  // Strip protocol prefix (ssh://, https://, http://, etc.)
808  let after_proto = s
809    .strip_prefix("ssh://")
810    .or_else(|| s.strip_prefix("https://"))
811    .or_else(|| s.strip_prefix("http://"))
812    .unwrap_or(s)
813    .trim_end_matches('/');
814
815  if after_proto.is_empty() || after_proto == "localhost" {
816    return Host::Localhost;
817  }
818
819  // Strip user@ prefix if present (e.g., "user@hostname" -> "hostname")
820  let hostname = after_proto
821    .split('@')
822    .next_back()
823    .unwrap_or(after_proto)
824    .trim();
825
826  if hostname.is_empty() || hostname == "localhost" {
827    Host::Localhost
828  } else {
829    Host::Remote(hostname.to_string())
830  }
831}
832
833fn extract_derivation_from_error(msg: &str) -> Option<String> {
834  extract_derivation_path(msg)
835}
836
837fn extract_file_name(msg: &str) -> Option<String> {
838  // Try to extract file name from evaluation messages
839  if let Some(start) = msg.find('\'')
840    && let Some(end) = msg[start + 1..].find('\'')
841  {
842    return Some(msg[start + 1..start + 1 + end].to_string());
843  }
844  None
845}
846
847fn parse_fail_type(msg: &str) -> FailType {
848  if msg.contains("timeout") {
849    FailType::Timeout
850  } else if msg.contains("hash mismatch") || msg.contains("hash") {
851    FailType::HashMismatch
852  } else if msg.contains("dependency failed") {
853    FailType::DependencyFailed
854  } else {
855    FailType::Unknown
856  }
857}
858
859fn find_derivation_by_activity(
860  state: &State,
861  activity_id: Id,
862) -> Option<DerivationId> {
863  // Try to find in running builds first
864  for (drv_id, build_info) in &state.full_summary.running_builds {
865    if build_info.activity_id == Some(activity_id) {
866      return Some(*drv_id);
867    }
868  }
869
870  // Search through all derivations
871  for (drv_id, info) in &state.derivation_infos {
872    match &info.build_status {
873      BuildStatus::Building(build_info)
874        if build_info.activity_id == Some(activity_id) =>
875      {
876        return Some(*drv_id);
877      },
878      BuildStatus::Built { info, .. }
879        if info.activity_id == Some(activity_id) =>
880      {
881        return Some(*drv_id);
882      },
883      BuildStatus::Failed { info, .. }
884        if info.activity_id == Some(activity_id) =>
885      {
886        return Some(*drv_id);
887      },
888      _ => {},
889    }
890  }
891
892  None
893}
894
895fn build_sort_order(state: &State, drv_id: DerivationId) -> (u8, i64) {
896  let Some(info) = state.get_derivation_info(drv_id) else {
897    return (9, 0);
898  };
899  match &info.build_status {
900    BuildStatus::Failed { fail, .. } => (0, (fail.at * 1_000_000.0) as i64),
901    BuildStatus::Building(build_info) => {
902      (1, (build_info.start * 1_000_000.0) as i64)
903    },
904    BuildStatus::Planned => (4, 0),
905    BuildStatus::Built { end, .. } => (6, -(*end * 1_000_000.0) as i64),
906    BuildStatus::Unknown => (9, 0),
907  }
908}
909
910fn best_subtree_sort_order(
911  state: &State,
912  drv_id: DerivationId,
913  depth: u8,
914) -> (u8, i64) {
915  let own = build_sort_order(state, drv_id);
916  if depth == 0 {
917    return own;
918  }
919  let Some(info) = state.get_derivation_info(drv_id) else {
920    return own;
921  };
922  let children: Vec<DerivationId> = info
923    .input_derivations
924    .iter()
925    .map(|d| d.derivation)
926    .collect();
927  children
928    .into_iter()
929    .map(|child_id| best_subtree_sort_order(state, child_id, depth - 1))
930    .fold(
931      own,
932      |best, candidate| {
933        if candidate < best { candidate } else { best }
934      },
935    )
936}
937
938fn sort_key(
939  state: &State,
940  drv_id: DerivationId,
941) -> (u8, i64, u8, i64, usize, usize, usize) {
942  let (own_a, own_b) = build_sort_order(state, drv_id);
943  let (sub_a, sub_b) = best_subtree_sort_order(state, drv_id, 20);
944
945  let summary = state
946    .get_derivation_info(drv_id)
947    .map(|i| &i.dependency_summary);
948
949  let running_builds = summary.map_or(0, |s| s.running_builds.len());
950  let running_downloads = summary.map_or(0, |s| s.running_downloads.len());
951  let planned =
952    summary.map_or(0, |s| s.planned_builds.len() + s.planned_downloads.len());
953
954  (
955    own_a,
956    own_b,
957    sub_a,
958    sub_b,
959    usize::MAX.saturating_sub(running_builds),
960    usize::MAX.saturating_sub(running_downloads),
961    planned,
962  )
963}
964
965fn sort_tree_children(state: &mut State, drv_id: DerivationId) {
966  let Some(info) = state.derivation_infos.get(&drv_id) else {
967    return;
968  };
969  let mut inputs: Vec<InputDerivation> = info.input_derivations.clone();
970  inputs.sort_by_key(|d| sort_key(state, d.derivation));
971
972  if let Some(info) = state.derivation_infos.get_mut(&drv_id) {
973    info.input_derivations = inputs;
974  }
975}
976
977pub fn detect_local_completed_builds(state: &mut State, now: f64) -> bool {
978  let local_building: Vec<DerivationId> = state
979    .full_summary
980    .running_builds
981    .iter()
982    .filter(|(_, info)| info.host == cognos::Host::Localhost)
983    .map(|(id, _)| *id)
984    .collect();
985
986  let mut any_completed = false;
987
988  for drv_id in local_building {
989    let output_paths: Vec<std::path::PathBuf> = state
990      .get_derivation_info(drv_id)
991      .map(|info| {
992        info
993          .outputs
994          .values()
995          .filter_map(|&sp_id| {
996            state
997              .get_store_path_info(sp_id)
998              .map(|sp_info| sp_info.name.path.clone())
999          })
1000          .collect()
1001      })
1002      .unwrap_or_default();
1003
1004    let all_exist =
1005      !output_paths.is_empty() && output_paths.iter().all(|p| p.exists());
1006    if all_exist {
1007      let build_info = state.get_derivation_info(drv_id).and_then(|info| {
1008        if let BuildStatus::Building(b) = &info.build_status {
1009          Some(b.clone())
1010        } else {
1011          None
1012        }
1013      });
1014
1015      if let Some(build_info) = build_info {
1016        let name = state
1017          .get_derivation_info(drv_id)
1018          .map(|i| i.name.name.clone())
1019          .unwrap_or_default();
1020        let platform = state
1021          .get_derivation_info(drv_id)
1022          .and_then(|i| i.platform.clone());
1023        let start = build_info.start;
1024        let host = build_info.host.clone();
1025        state.update_build_status(drv_id, BuildStatus::Built {
1026          info: build_info,
1027          end:  now,
1028        });
1029        record_build_completion(state, name, platform, start, now, &host);
1030        any_completed = true;
1031      }
1032    }
1033  }
1034
1035  any_completed
1036}
1037
1038/// Maintain state consistency
1039pub fn maintain_state(state: &mut State, _now: f64) {
1040  if !state.touched_ids.is_empty() {
1041    let touched: Vec<DerivationId> =
1042      state.touched_ids.iter().copied().collect();
1043    for drv_id in touched {
1044      sort_tree_children(state, drv_id);
1045    }
1046
1047    let roots: Vec<DerivationId> = state.forest_roots.clone();
1048    let mut sorted_roots = roots;
1049    sorted_roots.sort_by_key(|&id| sort_key(state, id));
1050    state.forest_roots = sorted_roots;
1051
1052    state.touched_ids.clear();
1053  }
1054}
1055
1056fn complete_build_success(state: &mut State, drv_id: DerivationId, now: f64) {
1057  let build_info = state.get_derivation_info(drv_id).and_then(|info| {
1058    if let BuildStatus::Building(build_info) = &info.build_status {
1059      Some(build_info.clone())
1060    } else {
1061      None
1062    }
1063  });
1064
1065  if let Some(build_info) = build_info {
1066    state.update_build_status(drv_id, BuildStatus::Built {
1067      info: build_info,
1068      end:  now,
1069    });
1070  }
1071}
1072
1073pub fn finish_state(state: &mut State) {
1074  state.progress_state = ProgressState::Finished;
1075
1076  let building: Vec<DerivationId> = state
1077    .derivation_infos
1078    .iter()
1079    .filter_map(|(drv_id, info)| {
1080      if matches!(info.build_status, BuildStatus::Building(_)) {
1081        Some(*drv_id)
1082      } else {
1083        None
1084      }
1085    })
1086    .collect();
1087
1088  for drv_id in building {
1089    complete_build_success(state, drv_id, current_time());
1090  }
1091
1092  let downloading: Vec<StorePathId> = state
1093    .full_summary
1094    .running_downloads
1095    .keys()
1096    .copied()
1097    .collect();
1098  for path_id in downloading {
1099    if let Some(transfer) =
1100      state.full_summary.running_downloads.remove(&path_id)
1101    {
1102      state.full_summary.completed_downloads.insert(
1103        path_id,
1104        CompletedTransferInfo {
1105          start:       transfer.start,
1106          end:         current_time(),
1107          host:        transfer.host,
1108          total_bytes: transfer.total_bytes.unwrap_or(0),
1109        },
1110      );
1111    }
1112  }
1113
1114  let uploading: Vec<StorePathId> =
1115    state.full_summary.running_uploads.keys().copied().collect();
1116  for path_id in uploading {
1117    if let Some(transfer) = state.full_summary.running_uploads.remove(&path_id)
1118    {
1119      state.full_summary.completed_uploads.insert(
1120        path_id,
1121        CompletedTransferInfo {
1122          start:       transfer.start,
1123          end:         current_time(),
1124          host:        transfer.host,
1125          total_bytes: transfer.total_bytes.unwrap_or(0),
1126        },
1127      );
1128    }
1129  }
1130}