1use 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
35pub fn process_message(state: &mut State, action: Actions) -> bool {
37 let now = current_time();
38 let mut changed = false;
39
40 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 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 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), 108 => handle_substitute_start(state, id, &text, &fields, now), 109 => handle_query_path_info_start(state, id, &text, &fields, now), 110 => handle_post_build_hook_start(state, id, &text, &fields, now), 101 => handle_file_transfer_start(state, id, &text, &fields, now), 100 => handle_copy_path_start(state, id, &text, &fields, now), 104 => {
117 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 true
129 },
130 _ => {
131 debug!("Unknown activity type: {}", activity_u8);
132 false
133 },
134 };
135
136 if changed && activity_u8 == 105 && parent_id.is_some() {
138 let parent_act_id = parent_id.unwrap();
139
140 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 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 if let Some(child_info) = state.get_derivation_info_mut(child_drv_id) {
169 child_info.derivation_parents.insert(parent_drv_id);
170 }
171 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), 108 => handle_substitute_stop(state, id, now), 101 | 100 => handle_transfer_stop(state, id, now), 109 | 110 => {
191 false
193 },
194 102 | 103 | 104 | 106 | 107 | 111 | 112 => {
195 false
198 },
199 _ => false,
200 }
201 } else {
202 false
203 }
204}
205
206fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool {
207 state.build_logs.push(msg.clone());
209
210 if let Some(phase_start) = msg.find("Running phase: ") {
212 let phase_name = &msg[phase_start + 15..]; let phase = phase_name.trim().to_string();
214
215 for activity in state.activities.values_mut() {
217 if activity.activity == 105 {
218 activity.phase = Some(phase.clone());
220 }
221 }
222 }
223
224 match level {
225 Verbosity::Error => {
226 if msg.contains("error:") || msg.contains("failed") {
228 state.nix_errors.push(msg.clone());
229
230 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 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 if msg.contains("evaluating") || msg.contains("copying") {
265 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 },
274 Verbosity::Talkative
275 | Verbosity::Chatty
276 | Verbosity::Debug
277 | Verbosity::Vomit => {
278 state.traces.push(msg);
280 true
281 },
282 _ => {
283 true },
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
383fn get_build_estimate(
385 state: &State,
386 derivation_name: &str,
387 host: &Host,
388) -> Option<u64> {
389 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
401fn 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 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 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 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 state.populate_derivation_dependencies(drv_id);
473 debug!(
474 "After populate_derivation_dependencies, state has {} derivations",
475 state.derivation_infos.len()
476 );
477
478 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 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 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 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 if fields.is_empty() {
610 debug!("FileTransfer activity {} has no fields", id);
611 return false;
612 }
613
614 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 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, activity_id: id,
656 bytes_transferred: 0,
657 total_bytes: None,
658 };
659
660 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 if fields.len() < 2 {
677 debug!("QueryPathInfo activity {} has insufficient fields", id);
678 return false;
679 }
680
681 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 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 return true;
704 }
705
706 false
707}
708
709fn handle_transfer_stop(state: &mut State, id: Id, now: f64) -> bool {
710 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 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 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 if let Some(start) = text.find("/nix/store/") {
767 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
777fn parse_host(s: &str) -> Host {
783 let s = s.trim();
784
785 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 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 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 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 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 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
1027pub 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}