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
711  for (path_id, transfer_info) in &state.full_summary.running_downloads.clone()
712  {
713    if transfer_info.activity_id == id {
714      state.full_summary.running_downloads.remove(path_id);
715
716      let completed = CompletedTransferInfo {
717        start:       transfer_info.start,
718        end:         now,
719        host:        transfer_info.host.clone(),
720        total_bytes: transfer_info.bytes_transferred,
721      };
722
723      state
724        .full_summary
725        .completed_downloads
726        .insert(*path_id, completed);
727      return true;
728    }
729  }
730
731  // Check uploads
732  for (path_id, transfer_info) in &state.full_summary.running_uploads.clone() {
733    if transfer_info.activity_id == id {
734      state.full_summary.running_uploads.remove(path_id);
735
736      let completed = CompletedTransferInfo {
737        start:       transfer_info.start,
738        end:         now,
739        host:        transfer_info.host.clone(),
740        total_bytes: transfer_info.bytes_transferred,
741      };
742
743      state
744        .full_summary
745        .completed_uploads
746        .insert(*path_id, completed);
747      return true;
748    }
749  }
750
751  false
752}
753
754fn extract_derivation_path(text: &str) -> Option<String> {
755  // Look for .drv paths in the text
756  if let Some(start) = text.find("/nix/store/")
757    && let Some(end) = text[start..].find(".drv")
758  {
759    return Some(text[start..start + end + 4].to_string());
760  }
761  None
762}
763
764fn extract_store_path(text: &str) -> Option<String> {
765  // Look for store paths in the text
766  if let Some(start) = text.find("/nix/store/") {
767    // Find the end of the path (space or end of string)
768    let rest = &text[start..];
769    let end = rest
770      .find(|c: char| c.is_whitespace() || c == '\'' || c == '"')
771      .unwrap_or(rest.len());
772    return Some(rest[..end].to_string());
773  }
774  None
775}
776
777/// Parse a host from the fields[1] string of a Build or Substitute activity.
778///
779/// - Empty string, "local", "local://", "unix", "unix://" -> Localhost
780/// - Properly strips proto:// prefix and extracts just the hostname
781/// - Strips user@ from user@host format
782fn parse_host(s: &str) -> Host {
783  let s = s.trim();
784
785  // Handle known localhost aliases
786  if s.is_empty()
787    || s == "localhost"
788    || s == "local"
789    || s == "local://"
790    || s == "unix"
791    || s == "unix://"
792  {
793    return Host::Localhost;
794  }
795
796  // Strip protocol prefix (ssh://, https://, http://, etc.)
797  let after_proto = s
798    .strip_prefix("ssh://")
799    .or_else(|| s.strip_prefix("https://"))
800    .or_else(|| s.strip_prefix("http://"))
801    .unwrap_or(s)
802    .trim_end_matches('/');
803
804  if after_proto.is_empty() || after_proto == "localhost" {
805    return Host::Localhost;
806  }
807
808  // Strip user@ prefix if present (e.g., "user@hostname" -> "hostname")
809  let hostname = after_proto
810    .split('@')
811    .next_back()
812    .unwrap_or(after_proto)
813    .trim();
814
815  if hostname.is_empty() || hostname == "localhost" {
816    Host::Localhost
817  } else {
818    Host::Remote(hostname.to_string())
819  }
820}
821
822fn extract_derivation_from_error(msg: &str) -> Option<String> {
823  extract_derivation_path(msg)
824}
825
826fn extract_file_name(msg: &str) -> Option<String> {
827  // Try to extract file name from evaluation messages
828  if let Some(start) = msg.find('\'')
829    && let Some(end) = msg[start + 1..].find('\'')
830  {
831    return Some(msg[start + 1..start + 1 + end].to_string());
832  }
833  None
834}
835
836fn parse_fail_type(msg: &str) -> FailType {
837  if msg.contains("timeout") {
838    FailType::Timeout
839  } else if msg.contains("hash mismatch") || msg.contains("hash") {
840    FailType::HashMismatch
841  } else if msg.contains("dependency failed") {
842    FailType::DependencyFailed
843  } else {
844    FailType::Unknown
845  }
846}
847
848fn find_derivation_by_activity(
849  state: &State,
850  activity_id: Id,
851) -> Option<DerivationId> {
852  // Try to find in running builds first
853  for (drv_id, build_info) in &state.full_summary.running_builds {
854    if build_info.activity_id == Some(activity_id) {
855      return Some(*drv_id);
856    }
857  }
858
859  // Search through all derivations
860  for (drv_id, info) in &state.derivation_infos {
861    match &info.build_status {
862      BuildStatus::Building(build_info)
863        if build_info.activity_id == Some(activity_id) =>
864      {
865        return Some(*drv_id);
866      },
867      BuildStatus::Built { info, .. }
868        if info.activity_id == Some(activity_id) =>
869      {
870        return Some(*drv_id);
871      },
872      BuildStatus::Failed { info, .. }
873        if info.activity_id == Some(activity_id) =>
874      {
875        return Some(*drv_id);
876      },
877      _ => {},
878    }
879  }
880
881  None
882}
883
884fn build_sort_order(state: &State, drv_id: DerivationId) -> (u8, i64) {
885  let Some(info) = state.get_derivation_info(drv_id) else {
886    return (9, 0);
887  };
888  match &info.build_status {
889    BuildStatus::Failed { fail, .. } => (0, (fail.at * 1_000_000.0) as i64),
890    BuildStatus::Building(build_info) => {
891      (1, (build_info.start * 1_000_000.0) as i64)
892    },
893    BuildStatus::Planned => (4, 0),
894    BuildStatus::Built { end, .. } => (6, -(*end * 1_000_000.0) as i64),
895    BuildStatus::Unknown => (9, 0),
896  }
897}
898
899fn best_subtree_sort_order(
900  state: &State,
901  drv_id: DerivationId,
902  depth: u8,
903) -> (u8, i64) {
904  let own = build_sort_order(state, drv_id);
905  if depth == 0 {
906    return own;
907  }
908  let Some(info) = state.get_derivation_info(drv_id) else {
909    return own;
910  };
911  let children: Vec<DerivationId> = info
912    .input_derivations
913    .iter()
914    .map(|d| d.derivation)
915    .collect();
916  children
917    .into_iter()
918    .map(|child_id| best_subtree_sort_order(state, child_id, depth - 1))
919    .fold(
920      own,
921      |best, candidate| {
922        if candidate < best { candidate } else { best }
923      },
924    )
925}
926
927fn sort_key(
928  state: &State,
929  drv_id: DerivationId,
930) -> (u8, i64, u8, i64, usize, usize, usize) {
931  let (own_a, own_b) = build_sort_order(state, drv_id);
932  let (sub_a, sub_b) = best_subtree_sort_order(state, drv_id, 20);
933
934  let summary = state
935    .get_derivation_info(drv_id)
936    .map(|i| &i.dependency_summary);
937
938  let running_builds = summary.map_or(0, |s| s.running_builds.len());
939  let running_downloads = summary.map_or(0, |s| s.running_downloads.len());
940  let planned =
941    summary.map_or(0, |s| s.planned_builds.len() + s.planned_downloads.len());
942
943  (
944    own_a,
945    own_b,
946    sub_a,
947    sub_b,
948    usize::MAX.saturating_sub(running_builds),
949    usize::MAX.saturating_sub(running_downloads),
950    planned,
951  )
952}
953
954fn sort_tree_children(state: &mut State, drv_id: DerivationId) {
955  let Some(info) = state.derivation_infos.get(&drv_id) else {
956    return;
957  };
958  let mut inputs: Vec<InputDerivation> = info.input_derivations.clone();
959  inputs.sort_by_key(|d| sort_key(state, d.derivation));
960
961  if let Some(info) = state.derivation_infos.get_mut(&drv_id) {
962    info.input_derivations = inputs;
963  }
964}
965
966pub fn detect_local_completed_builds(state: &mut State, now: f64) -> bool {
967  let local_building: Vec<DerivationId> = state
968    .full_summary
969    .running_builds
970    .iter()
971    .filter(|(_, info)| info.host == cognos::Host::Localhost)
972    .map(|(id, _)| *id)
973    .collect();
974
975  let mut any_completed = false;
976
977  for drv_id in local_building {
978    let output_paths: Vec<std::path::PathBuf> = state
979      .get_derivation_info(drv_id)
980      .map(|info| {
981        info
982          .outputs
983          .values()
984          .filter_map(|&sp_id| {
985            state
986              .get_store_path_info(sp_id)
987              .map(|sp_info| sp_info.name.path.clone())
988          })
989          .collect()
990      })
991      .unwrap_or_default();
992
993    let all_exist =
994      !output_paths.is_empty() && output_paths.iter().all(|p| p.exists());
995    if all_exist {
996      let build_info = state.get_derivation_info(drv_id).and_then(|info| {
997        if let BuildStatus::Building(b) = &info.build_status {
998          Some(b.clone())
999        } else {
1000          None
1001        }
1002      });
1003
1004      if let Some(build_info) = build_info {
1005        let name = state
1006          .get_derivation_info(drv_id)
1007          .map(|i| i.name.name.clone())
1008          .unwrap_or_default();
1009        let platform = state
1010          .get_derivation_info(drv_id)
1011          .and_then(|i| i.platform.clone());
1012        let start = build_info.start;
1013        let host = build_info.host.clone();
1014        state.update_build_status(drv_id, BuildStatus::Built {
1015          info: build_info,
1016          end:  now,
1017        });
1018        record_build_completion(state, name, platform, start, now, &host);
1019        any_completed = true;
1020      }
1021    }
1022  }
1023
1024  any_completed
1025}
1026
1027/// Maintain state consistency
1028pub fn maintain_state(state: &mut State, _now: f64) {
1029  if !state.touched_ids.is_empty() {
1030    let touched: Vec<DerivationId> =
1031      state.touched_ids.iter().copied().collect();
1032    for drv_id in touched {
1033      sort_tree_children(state, drv_id);
1034    }
1035
1036    let roots: Vec<DerivationId> = state.forest_roots.clone();
1037    let mut sorted_roots = roots;
1038    sorted_roots.sort_by_key(|&id| sort_key(state, id));
1039    state.forest_roots = sorted_roots;
1040
1041    state.touched_ids.clear();
1042  }
1043}
1044
1045fn complete_build_success(state: &mut State, drv_id: DerivationId, now: f64) {
1046  let build_info = state.get_derivation_info(drv_id).and_then(|info| {
1047    if let BuildStatus::Building(build_info) = &info.build_status {
1048      Some(build_info.clone())
1049    } else {
1050      None
1051    }
1052  });
1053
1054  if let Some(build_info) = build_info {
1055    state.update_build_status(drv_id, BuildStatus::Built {
1056      info: build_info,
1057      end:  now,
1058    });
1059  }
1060}
1061
1062pub fn finish_state(state: &mut State) {
1063  state.progress_state = ProgressState::Finished;
1064
1065  let building: Vec<DerivationId> = state
1066    .derivation_infos
1067    .iter()
1068    .filter_map(|(drv_id, info)| {
1069      if matches!(info.build_status, BuildStatus::Building(_)) {
1070        Some(*drv_id)
1071      } else {
1072        None
1073      }
1074    })
1075    .collect();
1076
1077  for drv_id in building {
1078    complete_build_success(state, drv_id, current_time());
1079  }
1080
1081  let downloading: Vec<StorePathId> = state
1082    .full_summary
1083    .running_downloads
1084    .keys()
1085    .copied()
1086    .collect();
1087  for path_id in downloading {
1088    if let Some(transfer) =
1089      state.full_summary.running_downloads.remove(&path_id)
1090    {
1091      state.full_summary.completed_downloads.insert(
1092        path_id,
1093        CompletedTransferInfo {
1094          start:       transfer.start,
1095          end:         current_time(),
1096          host:        transfer.host,
1097          total_bytes: transfer.total_bytes.unwrap_or(0),
1098        },
1099      );
1100    }
1101  }
1102
1103  let uploading: Vec<StorePathId> =
1104    state.full_summary.running_uploads.keys().copied().collect();
1105  for path_id in uploading {
1106    if let Some(transfer) = state.full_summary.running_uploads.remove(&path_id)
1107    {
1108      state.full_summary.completed_uploads.insert(
1109        path_id,
1110        CompletedTransferInfo {
1111          start:       transfer.start,
1112          end:         current_time(),
1113          host:        transfer.host,
1114          total_bytes: transfer.total_bytes.unwrap_or(0),
1115        },
1116      );
1117    }
1118  }
1119}