Compare commits
No commits in common. "27ecaac59cad45287ee436daa203f6b5746ac8c7" and "287dec65c39e6e78313e89498588ebec16227483" have entirely different histories.
27ecaac59c
...
287dec65c3
12 changed files with 340 additions and 1277 deletions
582
Cargo.lock
generated
582
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["NotAShelf <raf@notashelf.dev>"]
|
authors = ["NotAShelf <raf@notashelf.dev>"]
|
||||||
description = "Pretty build graphs for Nix builds"
|
description = "Pretty build graphs for Nix builds"
|
||||||
rust-version = "1.91.1"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
|
|
@ -19,7 +19,6 @@ crossterm = "0.29.0"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
chrono = "0.4.42"
|
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
|
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770115704,
|
"lastModified": 1765186076,
|
||||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ crossterm = "0.29"
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
indexmap.workspace = true
|
indexmap.workspace = true
|
||||||
csv.workspace = true
|
csv.workspace = true
|
||||||
chrono.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
397
rom/src/cache.rs
397
rom/src/cache.rs
|
|
@ -1,397 +0,0 @@
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
fs::{self, File, OpenOptions},
|
|
||||||
io::{BufReader, BufWriter},
|
|
||||||
path::PathBuf,
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use csv::{Reader, Writer};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::state::BuildReport;
|
|
||||||
|
|
||||||
/// Maximum number of historical builds to keep per derivation
|
|
||||||
const HISTORY_LIMIT: usize = 10;
|
|
||||||
|
|
||||||
/// Build report cache for CSV persistence
|
|
||||||
pub struct BuildReportCache {
|
|
||||||
cache_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CSV row format for build reports
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct BuildReportRow {
|
|
||||||
hostname: String,
|
|
||||||
derivation_name: String,
|
|
||||||
utc_time: String,
|
|
||||||
build_seconds: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BuildReportCache {
|
|
||||||
/// Create a new cache instance with the given path
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(cache_path: PathBuf) -> Self {
|
|
||||||
Self { cache_path }
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: just use the dirs crate for this
|
|
||||||
/// Get the default cache directory path
|
|
||||||
///
|
|
||||||
/// Uses `$XDG_STATE_HOME` if set, otherwise ``~/.local/state`
|
|
||||||
#[must_use]
|
|
||||||
pub fn default_cache_dir() -> PathBuf {
|
|
||||||
if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
|
|
||||||
PathBuf::from(xdg_state).join("rom")
|
|
||||||
} else if let Ok(home) = std::env::var("HOME") {
|
|
||||||
PathBuf::from(home).join(".local/state/rom")
|
|
||||||
} else {
|
|
||||||
PathBuf::from(".rom")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the default cache file path
|
|
||||||
#[must_use]
|
|
||||||
pub fn default_cache_path() -> PathBuf {
|
|
||||||
Self::default_cache_dir().join("build-reports.csv")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load build reports from CSV
|
|
||||||
///
|
|
||||||
/// Returns empty [`HashMap`] if file doesn't exist or parsing fails
|
|
||||||
pub fn load(&self) -> HashMap<(String, String), Vec<BuildReport>> {
|
|
||||||
if !self.cache_path.exists() {
|
|
||||||
return HashMap::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = match File::open(&self.cache_path) {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(_) => return HashMap::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
let mut csv_reader = Reader::from_reader(reader);
|
|
||||||
|
|
||||||
let mut reports: HashMap<(String, String), Vec<BuildReport>> =
|
|
||||||
HashMap::new();
|
|
||||||
|
|
||||||
for result in csv_reader.deserialize() {
|
|
||||||
let row: BuildReportRow = match result {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let completed_at = match parse_utc_time(&row.utc_time) {
|
|
||||||
Some(t) => t,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let report = BuildReport {
|
|
||||||
derivation_name: row.derivation_name.clone(),
|
|
||||||
platform: String::new(), // FIXME: not stored in CSV, for simplicity and because I'm lazy
|
|
||||||
duration_secs: row.build_seconds as f64,
|
|
||||||
completed_at,
|
|
||||||
host: row.hostname.clone(),
|
|
||||||
success: true, // only successful builds are cached
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = (row.hostname, row.derivation_name);
|
|
||||||
reports.entry(key).or_default().push(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort each entry by timestamp (newest first) and limit to HISTORY_LIMIT
|
|
||||||
for entries in reports.values_mut() {
|
|
||||||
entries.sort_by(|a, b| b.completed_at.cmp(&a.completed_at));
|
|
||||||
entries.truncate(HISTORY_LIMIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
reports
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save build reports to CSV
|
|
||||||
///
|
|
||||||
/// Merges with existing reports and enforces history limit
|
|
||||||
pub fn save(
|
|
||||||
&self,
|
|
||||||
reports: &HashMap<(String, String), Vec<BuildReport>>,
|
|
||||||
) -> Result<(), std::io::Error> {
|
|
||||||
// Ensure directory exists
|
|
||||||
if let Some(parent) = self.cache_path.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing reports to merge
|
|
||||||
let mut merged = self.load();
|
|
||||||
|
|
||||||
// Merge new reports
|
|
||||||
for ((host, drv_name), new_reports) in reports {
|
|
||||||
let key = (host.clone(), drv_name.clone());
|
|
||||||
let existing = merged.entry(key).or_default();
|
|
||||||
|
|
||||||
// Add new reports
|
|
||||||
existing.extend(new_reports.iter().cloned());
|
|
||||||
|
|
||||||
// Sort by timestamp (newest first)
|
|
||||||
existing.sort_by(|a, b| b.completed_at.cmp(&a.completed_at));
|
|
||||||
|
|
||||||
// Keep only most recent HISTORY_LIMIT entries
|
|
||||||
existing.truncate(HISTORY_LIMIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to CSV
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(&self.cache_path)?;
|
|
||||||
|
|
||||||
let writer = BufWriter::new(file);
|
|
||||||
let mut csv_writer = Writer::from_writer(writer);
|
|
||||||
|
|
||||||
// Flatten and write all reports
|
|
||||||
for ((hostname, derivation_name), entries) in merged {
|
|
||||||
for report in entries {
|
|
||||||
let row = BuildReportRow {
|
|
||||||
hostname: hostname.clone(),
|
|
||||||
derivation_name: derivation_name.clone(),
|
|
||||||
utc_time: format_utc_time(report.completed_at),
|
|
||||||
build_seconds: report.duration_secs as u64,
|
|
||||||
};
|
|
||||||
csv_writer.serialize(row)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
csv_writer.flush()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate median build time from historical reports
|
|
||||||
///
|
|
||||||
/// Returns [`None`] if there are no reports
|
|
||||||
#[must_use]
|
|
||||||
pub fn calculate_median(reports: &[BuildReport]) -> Option<u64> {
|
|
||||||
if reports.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut durations: Vec<u64> =
|
|
||||||
reports.iter().map(|r| r.duration_secs as u64).collect();
|
|
||||||
durations.sort_unstable();
|
|
||||||
|
|
||||||
let len = durations.len();
|
|
||||||
if len % 2 == 1 {
|
|
||||||
Some(durations[len / 2])
|
|
||||||
} else {
|
|
||||||
let mid1 = durations[len / 2 - 1];
|
|
||||||
let mid2 = durations[len / 2];
|
|
||||||
Some((mid1 + mid2) / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get median build time for a specific derivation on a host
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_estimate(
|
|
||||||
&self,
|
|
||||||
reports: &HashMap<(String, String), Vec<BuildReport>>,
|
|
||||||
host: &str,
|
|
||||||
derivation_name: &str,
|
|
||||||
) -> Option<u64> {
|
|
||||||
let key = (host.to_string(), derivation_name.to_string());
|
|
||||||
let entries = reports.get(&key)?;
|
|
||||||
Self::calculate_median(entries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse UTC time string in format "%Y-%m-%d %H:%M:%S"
|
|
||||||
fn parse_utc_time(s: &str) -> Option<SystemTime> {
|
|
||||||
// Simple parsing for "YYYY-MM-DD HH:MM:SS" format
|
|
||||||
let parts: Vec<&str> = s.split([' ', '-', ':']).collect();
|
|
||||||
if parts.len() != 6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let year: i64 = parts[0].parse().ok()?;
|
|
||||||
let month: u64 = parts[1].parse().ok()?;
|
|
||||||
let day: u64 = parts[2].parse().ok()?;
|
|
||||||
let hour: u64 = parts[3].parse().ok()?;
|
|
||||||
let minute: u64 = parts[4].parse().ok()?;
|
|
||||||
let second: u64 = parts[5].parse().ok()?;
|
|
||||||
|
|
||||||
// Approximate conversion to Unix timestamp
|
|
||||||
// This is a simplified calculation that doesn't handle leap years perfectly
|
|
||||||
let days_since_epoch = (year - 1970) * 365
|
|
||||||
+ (year - 1969) / 4
|
|
||||||
+ days_until_month(month)
|
|
||||||
+ day as i64
|
|
||||||
- 1;
|
|
||||||
let seconds_since_epoch =
|
|
||||||
days_since_epoch as u64 * 86400 + hour * 3600 + minute * 60 + second;
|
|
||||||
|
|
||||||
Some(
|
|
||||||
SystemTime::UNIX_EPOCH
|
|
||||||
+ std::time::Duration::from_secs(seconds_since_epoch),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: I'm really sure there's a library for this but lets just get
|
|
||||||
// this thing compiling
|
|
||||||
/// Calculate days until the start of a month (approximation)
|
|
||||||
const fn days_until_month(month: u64) -> i64 {
|
|
||||||
match month {
|
|
||||||
1 => 0,
|
|
||||||
2 => 31,
|
|
||||||
3 => 59,
|
|
||||||
4 => 90,
|
|
||||||
5 => 120,
|
|
||||||
6 => 151,
|
|
||||||
7 => 181,
|
|
||||||
8 => 212,
|
|
||||||
9 => 243,
|
|
||||||
10 => 273,
|
|
||||||
11 => 304,
|
|
||||||
12 => 334,
|
|
||||||
_ => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: does Chrono do this?
|
|
||||||
/// Format SystemTime as UTC string in format "%Y-%m-%d %H:%M:%S"
|
|
||||||
fn format_utc_time(time: SystemTime) -> String {
|
|
||||||
let duration = time
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let secs = duration.as_secs();
|
|
||||||
|
|
||||||
let days = secs / 86400;
|
|
||||||
let remaining = secs % 86400;
|
|
||||||
let hours = remaining / 3600;
|
|
||||||
let minutes = (remaining % 3600) / 60;
|
|
||||||
let seconds = remaining % 60;
|
|
||||||
|
|
||||||
// Approximate conversion from days since epoch to date
|
|
||||||
let mut year = 1970;
|
|
||||||
let mut days_left = days as i64;
|
|
||||||
|
|
||||||
// Subtract full years
|
|
||||||
while days_left >= 365 {
|
|
||||||
if is_leap_year(year) && days_left >= 366 {
|
|
||||||
days_left -= 366;
|
|
||||||
year += 1;
|
|
||||||
} else if !is_leap_year(year) {
|
|
||||||
days_left -= 365;
|
|
||||||
year += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate month and day
|
|
||||||
let (month, day) = calculate_month_day(days_left as u64, is_leap_year(year));
|
|
||||||
|
|
||||||
format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}")
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn is_leap_year(year: i64) -> bool {
|
|
||||||
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_month_day(days: u64, is_leap: bool) -> (u8, u8) {
|
|
||||||
let days_in_month: [u8; 12] = if is_leap {
|
|
||||||
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
||||||
} else {
|
|
||||||
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut remaining = days as i32;
|
|
||||||
for (i, &month_days) in days_in_month.iter().enumerate() {
|
|
||||||
if remaining < i32::from(month_days) {
|
|
||||||
return ((i + 1) as u8, (remaining + 1) as u8);
|
|
||||||
}
|
|
||||||
remaining -= i32::from(month_days);
|
|
||||||
}
|
|
||||||
|
|
||||||
(12, 31)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_calculate_median_odd() {
|
|
||||||
let reports = vec![
|
|
||||||
BuildReport {
|
|
||||||
derivation_name: "test".to_string(),
|
|
||||||
platform: "x86_64-linux".to_string(),
|
|
||||||
duration_secs: 10.0,
|
|
||||||
completed_at: SystemTime::UNIX_EPOCH,
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
BuildReport {
|
|
||||||
derivation_name: "test".to_string(),
|
|
||||||
platform: "x86_64-linux".to_string(),
|
|
||||||
duration_secs: 20.0,
|
|
||||||
completed_at: SystemTime::UNIX_EPOCH,
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
BuildReport {
|
|
||||||
derivation_name: "test".to_string(),
|
|
||||||
platform: "x86_64-linux".to_string(),
|
|
||||||
duration_secs: 30.0,
|
|
||||||
completed_at: SystemTime::UNIX_EPOCH,
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(BuildReportCache::calculate_median(&reports), Some(20));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_calculate_median_even() {
|
|
||||||
let reports = vec![
|
|
||||||
BuildReport {
|
|
||||||
derivation_name: "test".to_string(),
|
|
||||||
platform: "x86_64-linux".to_string(),
|
|
||||||
duration_secs: 10.0,
|
|
||||||
completed_at: SystemTime::UNIX_EPOCH,
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
BuildReport {
|
|
||||||
derivation_name: "test".to_string(),
|
|
||||||
platform: "x86_64-linux".to_string(),
|
|
||||||
duration_secs: 20.0,
|
|
||||||
completed_at: SystemTime::UNIX_EPOCH,
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(BuildReportCache::calculate_median(&reports), Some(15));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_calculate_median_empty() {
|
|
||||||
let reports = vec![];
|
|
||||||
assert_eq!(BuildReportCache::calculate_median(&reports), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_parse_utc_time() {
|
|
||||||
let time =
|
|
||||||
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
|
|
||||||
let formatted = format_utc_time(time);
|
|
||||||
let parsed = parse_utc_time(&formatted).unwrap();
|
|
||||||
|
|
||||||
// Allow small difference due to approximation
|
|
||||||
let diff = parsed
|
|
||||||
.duration_since(time)
|
|
||||||
.unwrap_or_else(|e| e.duration())
|
|
||||||
.as_secs();
|
|
||||||
assert!(diff < 86400); // less than 1 day difference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
131
rom/src/cli.rs
131
rom/src/cli.rs
|
|
@ -33,14 +33,6 @@ pub struct Cli {
|
||||||
/// Summary display style: concise, table, full
|
/// Summary display style: concise, table, full
|
||||||
#[arg(long, global = true, default_value = "concise")]
|
#[arg(long, global = true, default_value = "concise")]
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
|
|
||||||
/// Log prefix style: short, full, none
|
|
||||||
#[arg(long, global = true, default_value = "short")]
|
|
||||||
pub log_prefix: String,
|
|
||||||
|
|
||||||
/// Maximum number of log lines to display
|
|
||||||
#[arg(long, global = true)]
|
|
||||||
pub log_lines: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
#[derive(Debug, clap::Subcommand)]
|
||||||
|
|
@ -94,8 +86,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
cli.format.clone(),
|
cli.format.clone(),
|
||||||
cli.legend.clone(),
|
cli.legend.clone(),
|
||||||
cli.summary.clone(),
|
cli.summary.clone(),
|
||||||
cli.log_prefix.clone(),
|
|
||||||
cli.log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -111,8 +101,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
cli.format.clone(),
|
cli.format.clone(),
|
||||||
cli.legend.clone(),
|
cli.legend.clone(),
|
||||||
cli.summary.clone(),
|
cli.summary.clone(),
|
||||||
cli.log_prefix.clone(),
|
|
||||||
cli.log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -130,10 +118,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||||
legend_style: cli.legend.clone(),
|
legend_style: cli.legend.clone(),
|
||||||
summary_style: cli.summary.clone(),
|
summary_style: cli.summary.clone(),
|
||||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
|
||||||
&cli.log_prefix,
|
|
||||||
),
|
|
||||||
log_line_limit: cli.log_lines,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
|
|
@ -156,8 +140,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
cli.format.clone(),
|
cli.format.clone(),
|
||||||
cli.legend.clone(),
|
cli.legend.clone(),
|
||||||
cli.summary.clone(),
|
cli.summary.clone(),
|
||||||
cli.log_prefix.clone(),
|
|
||||||
cli.log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -175,10 +157,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||||
legend_style: cli.legend.clone(),
|
legend_style: cli.legend.clone(),
|
||||||
summary_style: cli.summary.clone(),
|
summary_style: cli.summary.clone(),
|
||||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
|
||||||
&cli.log_prefix,
|
|
||||||
),
|
|
||||||
log_line_limit: cli.log_lines,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
|
|
@ -201,8 +179,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
cli.format.clone(),
|
cli.format.clone(),
|
||||||
cli.legend.clone(),
|
cli.legend.clone(),
|
||||||
cli.summary.clone(),
|
cli.summary.clone(),
|
||||||
cli.log_prefix.clone(),
|
|
||||||
cli.log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -220,10 +196,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||||
legend_style: cli.legend.clone(),
|
legend_style: cli.legend.clone(),
|
||||||
summary_style: cli.summary.clone(),
|
summary_style: cli.summary.clone(),
|
||||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
|
||||||
&cli.log_prefix,
|
|
||||||
),
|
|
||||||
log_line_limit: cli.log_lines,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
|
|
@ -246,8 +218,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
cli.format.clone(),
|
cli.format.clone(),
|
||||||
cli.legend.clone(),
|
cli.legend.clone(),
|
||||||
cli.summary.clone(),
|
cli.summary.clone(),
|
||||||
cli.log_prefix.clone(),
|
|
||||||
cli.log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -269,10 +239,6 @@ pub fn run() -> eyre::Result<()> {
|
||||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||||
legend_style: cli.legend.clone(),
|
legend_style: cli.legend.clone(),
|
||||||
summary_style: cli.summary.clone(),
|
summary_style: cli.summary.clone(),
|
||||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
|
||||||
&cli.log_prefix,
|
|
||||||
),
|
|
||||||
log_line_limit: cli.log_lines,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
|
|
@ -314,8 +280,6 @@ fn run_nix_build_wrapper(
|
||||||
format: String,
|
format: String,
|
||||||
legend_style: String,
|
legend_style: String,
|
||||||
summary_style: String,
|
summary_style: String,
|
||||||
log_prefix: String,
|
|
||||||
log_lines: Option<usize>,
|
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// Validate that at least one package/flake is specified
|
// Validate that at least one package/flake is specified
|
||||||
if package_and_rom_args.is_empty() {
|
if package_and_rom_args.is_empty() {
|
||||||
|
|
@ -346,8 +310,6 @@ fn run_nix_build_wrapper(
|
||||||
format,
|
format,
|
||||||
legend_style,
|
legend_style,
|
||||||
summary_style,
|
summary_style,
|
||||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
|
||||||
log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
std::process::exit(exit_code);
|
std::process::exit(exit_code);
|
||||||
|
|
@ -363,8 +325,6 @@ fn run_nix_shell_wrapper(
|
||||||
format: String,
|
format: String,
|
||||||
legend_style: String,
|
legend_style: String,
|
||||||
summary_style: String,
|
summary_style: String,
|
||||||
log_prefix: String,
|
|
||||||
log_lines: Option<usize>,
|
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// Validate that at least one package/flake is specified
|
// Validate that at least one package/flake is specified
|
||||||
if package_and_rom_args.is_empty() {
|
if package_and_rom_args.is_empty() {
|
||||||
|
|
@ -400,8 +360,6 @@ fn run_nix_shell_wrapper(
|
||||||
format,
|
format,
|
||||||
legend_style,
|
legend_style,
|
||||||
summary_style,
|
summary_style,
|
||||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
|
||||||
log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
|
|
@ -433,8 +391,6 @@ fn run_nix_develop_wrapper(
|
||||||
format: String,
|
format: String,
|
||||||
legend_style: String,
|
legend_style: String,
|
||||||
summary_style: String,
|
summary_style: String,
|
||||||
log_prefix: String,
|
|
||||||
log_lines: Option<usize>,
|
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// Validate that at least one package/flake is specified (can be empty for
|
// Validate that at least one package/flake is specified (can be empty for
|
||||||
// current flake) develop without args is valid (uses current directory's
|
// current flake) develop without args is valid (uses current directory's
|
||||||
|
|
@ -463,8 +419,6 @@ fn run_nix_develop_wrapper(
|
||||||
format,
|
format,
|
||||||
legend_style,
|
legend_style,
|
||||||
summary_style,
|
summary_style,
|
||||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
|
||||||
log_lines,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
|
|
@ -496,8 +450,6 @@ fn run_monitored_command(
|
||||||
format_str: String,
|
format_str: String,
|
||||||
legend_style_str: String,
|
legend_style_str: String,
|
||||||
summary_style_str: String,
|
summary_style_str: String,
|
||||||
log_prefix_style: crate::types::LogPrefixStyle,
|
|
||||||
log_line_limit: Option<usize>,
|
|
||||||
) -> eyre::Result<i32> {
|
) -> eyre::Result<i32> {
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
|
|
@ -529,13 +481,6 @@ fn run_monitored_command(
|
||||||
let start_time = Arc::new(Mutex::new(crate::state::current_time()));
|
let start_time = Arc::new(Mutex::new(crate::state::current_time()));
|
||||||
let start_time_clone = start_time.clone();
|
let start_time_clone = start_time.clone();
|
||||||
|
|
||||||
// Buffer for build logs - collected and passed to Display for coordinated
|
|
||||||
// rendering
|
|
||||||
let log_buffer =
|
|
||||||
Arc::new(Mutex::new(std::collections::VecDeque::<String>::new()));
|
|
||||||
let log_buffer_clone = log_buffer.clone();
|
|
||||||
let log_buffer_render = log_buffer.clone();
|
|
||||||
|
|
||||||
// Spawn thread to read and parse stderr (where nix outputs logs)
|
// Spawn thread to read and parse stderr (where nix outputs logs)
|
||||||
let stderr_thread = thread::spawn(move || {
|
let stderr_thread = thread::spawn(move || {
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
@ -550,62 +495,19 @@ fn run_monitored_command(
|
||||||
if let Ok(action) = serde_json::from_str::<cognos::Actions>(json_line) {
|
if let Ok(action) = serde_json::from_str::<cognos::Actions>(json_line) {
|
||||||
debug!("Parsed JSON message #{}: {:?}", json_count, action);
|
debug!("Parsed JSON message #{}: {:?}", json_count, action);
|
||||||
|
|
||||||
// Process the action first to update state
|
// Print messages immediately to stdout
|
||||||
|
if let cognos::Actions::Message { msg, .. } = &action {
|
||||||
|
println!("{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
let mut state = state_clone.lock().unwrap();
|
let mut state = state_clone.lock().unwrap();
|
||||||
let derivation_count_before = state.derivation_infos.len();
|
let derivation_count_before = state.derivation_infos.len();
|
||||||
crate::update::process_message(&mut state, action.clone());
|
crate::update::process_message(&mut state, action);
|
||||||
crate::update::maintain_state(
|
crate::update::maintain_state(
|
||||||
&mut state,
|
&mut state,
|
||||||
crate::state::current_time(),
|
crate::state::current_time(),
|
||||||
);
|
);
|
||||||
let derivation_count_after = state.derivation_infos.len();
|
let derivation_count_after = state.derivation_infos.len();
|
||||||
|
|
||||||
// Now handle build log messages after state is updated
|
|
||||||
// Buffer them for coordinated rendering with the display
|
|
||||||
match &action {
|
|
||||||
cognos::Actions::Message { msg, .. } => {
|
|
||||||
let mut logs = log_buffer_clone.lock().unwrap();
|
|
||||||
logs.push_back(msg.clone());
|
|
||||||
// Keep only recent logs based on limit
|
|
||||||
if let Some(limit) = log_line_limit {
|
|
||||||
while logs.len() > limit {
|
|
||||||
logs.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cognos::Actions::Result {
|
|
||||||
fields,
|
|
||||||
activity,
|
|
||||||
id,
|
|
||||||
} => {
|
|
||||||
// Build log lines come as Result actions with FileTransfer
|
|
||||||
// activity (101) and fields containing just the log
|
|
||||||
// text: fields = ["log line text"]
|
|
||||||
if matches!(activity, cognos::Activities::FileTransfer)
|
|
||||||
&& !fields.is_empty()
|
|
||||||
{
|
|
||||||
if let Some(log_text) = fields[0].as_str() {
|
|
||||||
// Get the activity prefix (e.g., "hello> ")
|
|
||||||
let use_color = !silent;
|
|
||||||
let prefix = state
|
|
||||||
.get_activity_prefix(*id, &log_prefix_style, use_color)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let prefixed_log = format!("{prefix}{log_text}");
|
|
||||||
let mut logs = log_buffer_clone.lock().unwrap();
|
|
||||||
logs.push_back(prefixed_log);
|
|
||||||
// Keep only recent logs based on limit
|
|
||||||
if let Some(limit) = log_line_limit {
|
|
||||||
while logs.len() > limit {
|
|
||||||
logs.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if derivation_count_after != derivation_count_before {
|
if derivation_count_after != derivation_count_before {
|
||||||
debug!(
|
debug!(
|
||||||
"Derivation count changed: {} -> {}",
|
"Derivation count changed: {} -> {}",
|
||||||
|
|
@ -616,16 +518,9 @@ fn run_monitored_command(
|
||||||
debug!("Failed to parse JSON: {}", json_line);
|
debug!("Failed to parse JSON: {}", json_line);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-JSON lines, buffer them
|
// Non-JSON lines, pass through
|
||||||
non_json_count += 1;
|
non_json_count += 1;
|
||||||
let mut logs = log_buffer_clone.lock().unwrap();
|
println!("{line}");
|
||||||
logs.push_back(line.clone());
|
|
||||||
// Keep only recent logs based on limit
|
|
||||||
if let Some(limit) = log_line_limit {
|
|
||||||
while logs.len() > limit {
|
|
||||||
logs.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!(
|
debug!(
|
||||||
|
|
@ -688,16 +583,13 @@ fn run_monitored_command(
|
||||||
|| !state.full_summary.planned_builds.is_empty();
|
|| !state.full_summary.planned_builds.is_empty();
|
||||||
|
|
||||||
if !silent {
|
if !silent {
|
||||||
// Get buffered logs for coordinated rendering
|
|
||||||
let logs: Vec<String> =
|
|
||||||
log_buffer_render.lock().unwrap().iter().cloned().collect();
|
|
||||||
|
|
||||||
if has_activity || state.progress_state != ProgressState::JustStarted {
|
if has_activity || state.progress_state != ProgressState::JustStarted {
|
||||||
// Clear any previous timer display
|
// Clear any previous timer display
|
||||||
if last_timer_display.is_some() {
|
if last_timer_display.is_some() {
|
||||||
|
display.clear_previous().ok();
|
||||||
last_timer_display = None;
|
last_timer_display = None;
|
||||||
}
|
}
|
||||||
let _ = display.render(&state, &logs);
|
let _ = display.render(&state, &[]);
|
||||||
} else {
|
} else {
|
||||||
// Show initial timer while waiting for activity
|
// Show initial timer while waiting for activity
|
||||||
let start = *start_time_clone.lock().unwrap();
|
let start = *start_time_clone.lock().unwrap();
|
||||||
|
|
@ -707,7 +599,8 @@ fn run_monitored_command(
|
||||||
|
|
||||||
// Only update if changed (to avoid flicker)
|
// Only update if changed (to avoid flicker)
|
||||||
if last_timer_display.as_ref() != Some(&timer_text) {
|
if last_timer_display.as_ref() != Some(&timer_text) {
|
||||||
let _ = display.render(&state, &logs);
|
display.clear_previous().ok();
|
||||||
|
eprintln!("{timer_text}");
|
||||||
last_timer_display = Some(timer_text);
|
last_timer_display = Some(timer_text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ impl<W: Write> Display<W> {
|
||||||
|
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
// Print build logs ABOVE the graph
|
// Print accumulated logs first (these go above the tree)
|
||||||
for log in logs {
|
for log in logs {
|
||||||
lines.push(log.clone());
|
lines.push(log.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -153,8 +153,6 @@ impl<W: Write> Display<W> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_final(&mut self, state: &State) -> io::Result<()> {
|
pub fn render_final(&mut self, state: &State) -> io::Result<()> {
|
||||||
tracing::debug!("render_final called");
|
|
||||||
|
|
||||||
// Clear any previous render
|
// Clear any previous render
|
||||||
self.clear_previous()?;
|
self.clear_previous()?;
|
||||||
|
|
||||||
|
|
@ -182,8 +180,6 @@ impl<W: Write> Display<W> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("render_final: {} lines to print", lines.len());
|
|
||||||
|
|
||||||
// Print final output (don't track last_lines since this is final)
|
// Print final output (don't track last_lines since this is final)
|
||||||
for line in lines {
|
for line in lines {
|
||||||
writeln!(self.writer, "{line}")?;
|
writeln!(self.writer, "{line}")?;
|
||||||
|
|
@ -211,10 +207,8 @@ impl<W: Write> Display<W> {
|
||||||
let failed = state.full_summary.failed_builds.len();
|
let failed = state.full_summary.failed_builds.len();
|
||||||
let planned = state.full_summary.planned_builds.len();
|
let planned = state.full_summary.planned_builds.len();
|
||||||
|
|
||||||
let duration = current_time() - state.start_time;
|
|
||||||
|
|
||||||
// Always print summary (like NOM's "Finished at HH:MM:SS after Xs")
|
|
||||||
if running > 0 || completed > 0 || failed > 0 || planned > 0 {
|
if running > 0 || completed > 0 || failed > 0 || planned > 0 {
|
||||||
|
let duration = current_time() - state.start_time;
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"{} {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
|
"{} {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
|
||||||
self.colored("━", Color::Blue),
|
self.colored("━", Color::Blue),
|
||||||
|
|
@ -229,18 +223,6 @@ impl<W: Write> Display<W> {
|
||||||
self.colored("⏱", Color::Grey),
|
self.colored("⏱", Color::Grey),
|
||||||
self.format_duration(duration)
|
self.format_duration(duration)
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
// Nothing built - just show "Finished after Xs"
|
|
||||||
let now = chrono::Local::now();
|
|
||||||
let time_str = now.format("%H:%M:%S");
|
|
||||||
lines.push(format!(
|
|
||||||
"{} {}",
|
|
||||||
self.colored(&format!("Finished at {time_str}"), Color::Green),
|
|
||||||
self.colored(
|
|
||||||
&format!("after {}", self.format_duration(duration)),
|
|
||||||
Color::Green
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines
|
lines
|
||||||
|
|
@ -698,23 +680,11 @@ impl<W: Write> Display<W> {
|
||||||
if let Some(info) = state.get_derivation_info(*drv_id) {
|
if let Some(info) = state.get_derivation_info(*drv_id) {
|
||||||
let name = &info.name.name;
|
let name = &info.name.name;
|
||||||
let elapsed = current_time() - build.start;
|
let elapsed = current_time() - build.start;
|
||||||
|
|
||||||
// Format time info
|
|
||||||
let mut time_info = String::new();
|
|
||||||
if let Some(estimate_secs) = build.estimate {
|
|
||||||
let remaining = estimate_secs.saturating_sub(elapsed as u64);
|
|
||||||
time_info.push_str(&format!(
|
|
||||||
"∅ {} ",
|
|
||||||
self.format_duration(remaining as f64)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
time_info.push_str(&self.format_duration(elapsed));
|
|
||||||
|
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
" {} {} {}",
|
" {} {} {}",
|
||||||
self.colored("⏵", Color::Yellow),
|
self.colored("⏵", Color::Yellow),
|
||||||
name,
|
name,
|
||||||
time_info
|
self.format_duration(elapsed)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -984,19 +954,8 @@ impl<W: Write> Display<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time information
|
// Time elapsed
|
||||||
let elapsed = current_time() - build_info.start;
|
let elapsed = current_time() - build_info.start;
|
||||||
|
|
||||||
// Show estimate if available
|
|
||||||
if let Some(estimate_secs) = build_info.estimate {
|
|
||||||
let remaining = estimate_secs.saturating_sub(elapsed as u64);
|
|
||||||
line.push_str(&self.colored(
|
|
||||||
&format!(" ∅ {}", self.format_duration(remaining as f64)),
|
|
||||||
Color::DarkGrey,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show elapsed time
|
|
||||||
line.push_str(&self.colored(
|
line.push_str(&self.colored(
|
||||||
&format!(" ⏱ {}", self.format_duration(elapsed)),
|
&format!(" ⏱ {}", self.format_duration(elapsed)),
|
||||||
Color::DarkGrey,
|
Color::DarkGrey,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//! ROM - Rust Output Monitor
|
//! ROM - Rust Output Monitor
|
||||||
pub mod cache;
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
//! Monitor module for orchestrating state updates and display rendering
|
//! Monitor module for orchestrating state updates and display rendering
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, Write},
|
io::{BufRead, Write},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use cognos::Host;
|
use cognos::Host;
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::BuildReportCache,
|
|
||||||
display::{Display, DisplayConfig, LegendStyle, SummaryStyle},
|
display::{Display, DisplayConfig, LegendStyle, SummaryStyle},
|
||||||
error::{Result, RomError},
|
error::{Result, RomError},
|
||||||
state::{
|
state::{
|
||||||
|
|
@ -57,12 +54,7 @@ impl<W: Write> Monitor<W> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let display = Display::new(writer, display_config)?;
|
let display = Display::new(writer, display_config)?;
|
||||||
let mut state = State::new();
|
let state = State::new();
|
||||||
|
|
||||||
// Load build cache for predictions
|
|
||||||
let cache_path = BuildReportCache::default_cache_path();
|
|
||||||
let cache = BuildReportCache::new(cache_path);
|
|
||||||
state.build_cache = cache.load();
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state,
|
state,
|
||||||
|
|
@ -98,14 +90,6 @@ impl<W: Write> Monitor<W> {
|
||||||
self.display.render_final(&self.state)?;
|
self.display.render_final(&self.state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save build cache for future predictions
|
|
||||||
let cache_path = BuildReportCache::default_cache_path();
|
|
||||||
let cache = BuildReportCache::new(cache_path);
|
|
||||||
if let Err(e) = cache.save(&self.state.build_cache) {
|
|
||||||
debug!("Failed to save build cache: {}", e);
|
|
||||||
// Don't fail the build if cache save fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error code if there were failures
|
// Return error code if there were failures
|
||||||
if self.state.has_errors() {
|
if self.state.has_errors() {
|
||||||
return Err(RomError::BuildFailed);
|
return Err(RomError::BuildFailed);
|
||||||
|
|
@ -156,6 +140,10 @@ impl<W: Write> Monitor<W> {
|
||||||
|
|
||||||
/// Process a human-readable line
|
/// Process a human-readable line
|
||||||
fn process_human_line(&mut self, line: &str) -> Result<bool> {
|
fn process_human_line(&mut self, line: &str) -> Result<bool> {
|
||||||
|
// Parse human-readable nix output
|
||||||
|
// This is a simplified version - the full implementation would need
|
||||||
|
// comprehensive parsing of nix's output format
|
||||||
|
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
|
|
||||||
// Skip empty lines
|
// Skip empty lines
|
||||||
|
|
@ -282,8 +270,10 @@ impl<W: Write> Monitor<W> {
|
||||||
// Extract number of paths if present
|
// Extract number of paths if present
|
||||||
let words: Vec<&str> = line.split_whitespace().collect();
|
let words: Vec<&str> = line.split_whitespace().collect();
|
||||||
if words.len() >= 2 {
|
if words.len() >= 2 {
|
||||||
if let Ok(count) = words[1].parse::<usize>() {
|
if let Ok(_count) = words[1].parse::<usize>() {
|
||||||
debug!("Copying {} paths", count);
|
// XXX: This is a PlanCopies message, we'll probably track this
|
||||||
|
// For now just acknowledge it, and let future work decide how
|
||||||
|
// we should go around doing it.
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
rom/src/state.rs
106
rom/src/state.rs
|
|
@ -360,7 +360,6 @@ pub struct State {
|
||||||
pub full_summary: DependencySummary,
|
pub full_summary: DependencySummary,
|
||||||
pub forest_roots: Vec<DerivationId>,
|
pub forest_roots: Vec<DerivationId>,
|
||||||
pub build_reports: HashMap<String, Vec<BuildReport>>,
|
pub build_reports: HashMap<String, Vec<BuildReport>>,
|
||||||
pub build_cache: HashMap<(String, String), Vec<BuildReport>>,
|
|
||||||
pub start_time: f64,
|
pub start_time: f64,
|
||||||
pub progress_state: ProgressState,
|
pub progress_state: ProgressState,
|
||||||
pub store_path_ids: HashMap<StorePath, StorePathId>,
|
pub store_path_ids: HashMap<StorePath, StorePathId>,
|
||||||
|
|
@ -372,8 +371,6 @@ pub struct State {
|
||||||
pub traces: Vec<String>,
|
pub traces: Vec<String>,
|
||||||
pub build_platform: Option<String>,
|
pub build_platform: Option<String>,
|
||||||
pub evaluation_state: EvalInfo,
|
pub evaluation_state: EvalInfo,
|
||||||
pub builds_activity: Option<ActivityId>,
|
|
||||||
pub success_tokens: u64,
|
|
||||||
next_store_path_id: StorePathId,
|
next_store_path_id: StorePathId,
|
||||||
next_derivation_id: DerivationId,
|
next_derivation_id: DerivationId,
|
||||||
}
|
}
|
||||||
|
|
@ -393,7 +390,6 @@ impl State {
|
||||||
full_summary: DependencySummary::default(),
|
full_summary: DependencySummary::default(),
|
||||||
forest_roots: Vec::new(),
|
forest_roots: Vec::new(),
|
||||||
build_reports: HashMap::new(),
|
build_reports: HashMap::new(),
|
||||||
build_cache: HashMap::new(),
|
|
||||||
start_time: current_time(),
|
start_time: current_time(),
|
||||||
progress_state: ProgressState::JustStarted,
|
progress_state: ProgressState::JustStarted,
|
||||||
store_path_ids: HashMap::new(),
|
store_path_ids: HashMap::new(),
|
||||||
|
|
@ -405,8 +401,6 @@ impl State {
|
||||||
traces: Vec::new(),
|
traces: Vec::new(),
|
||||||
build_platform: None,
|
build_platform: None,
|
||||||
evaluation_state: EvalInfo::default(),
|
evaluation_state: EvalInfo::default(),
|
||||||
builds_activity: None,
|
|
||||||
success_tokens: 0,
|
|
||||||
next_store_path_id: 0,
|
next_store_path_id: 0,
|
||||||
next_derivation_id: 0,
|
next_derivation_id: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -703,106 +697,6 @@ impl State {
|
||||||
.copied()
|
.copied()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the activity prefix for a given activity ID by walking up the parent
|
|
||||||
/// chain to find a Build activity and extracting its derivation name.
|
|
||||||
/// Returns a prefix like "hello> " suitable for prepending to log lines.
|
|
||||||
/// If `use_color` is true and stderr is a TTY, the prefix will be blue.
|
|
||||||
/// The `prefix_style` determines whether to use short (pname only), full, or
|
|
||||||
/// no prefix.
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_activity_prefix(
|
|
||||||
&self,
|
|
||||||
activity_id: ActivityId,
|
|
||||||
prefix_style: &crate::types::LogPrefixStyle,
|
|
||||||
use_color: bool,
|
|
||||||
) -> Option<String> {
|
|
||||||
use cognos::Activities;
|
|
||||||
|
|
||||||
use crate::types::LogPrefixStyle;
|
|
||||||
|
|
||||||
// If prefix style is None, return empty string
|
|
||||||
if matches!(prefix_style, LogPrefixStyle::None) {
|
|
||||||
return Some(String::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut current_id = activity_id;
|
|
||||||
let max_depth = 10; // Prevent infinite loops
|
|
||||||
let mut depth = 0;
|
|
||||||
|
|
||||||
while depth < max_depth {
|
|
||||||
if let Some(activity) = self.activities.get(¤t_id) {
|
|
||||||
// Check if this is a Build activity (type 105)
|
|
||||||
if activity.activity == Activities::Build as u8 {
|
|
||||||
// Extract derivation path from the text field
|
|
||||||
// The text field typically contains something like:
|
|
||||||
// "building '/nix/store/...-hello-2.10.drv'"
|
|
||||||
if let Some(drv) = extract_derivation_from_text(&activity.text) {
|
|
||||||
// Look up the DerivationInfo for this derivation
|
|
||||||
let drv_id = self.derivation_ids.get(&drv);
|
|
||||||
let name = if matches!(prefix_style, LogPrefixStyle::Short) {
|
|
||||||
// Try to use pname if available
|
|
||||||
if let Some(id) = drv_id {
|
|
||||||
if let Some(drv_info) = self.derivation_infos.get(id) {
|
|
||||||
if let Some(pname) = &drv_info.pname {
|
|
||||||
pname.clone()
|
|
||||||
} else {
|
|
||||||
drv.name.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drv.name.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drv.name.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Full style - use full derivation name
|
|
||||||
drv.name.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply color if requested and stderr is a TTY
|
|
||||||
let colored_name = if use_color
|
|
||||||
&& std::io::IsTerminal::is_terminal(&std::io::stderr())
|
|
||||||
{
|
|
||||||
format!("\x1b[34m{name}\x1b[0m")
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(format!("{colored_name}> "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to parent activity
|
|
||||||
if let Some(parent_id) = activity.parent {
|
|
||||||
if parent_id == 0 {
|
|
||||||
break; // Reached root
|
|
||||||
}
|
|
||||||
current_id = parent_id;
|
|
||||||
depth += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract derivation from activity text like "building
|
|
||||||
/// '/nix/store/...-hello-2.10.drv'" Returns the Derivation object
|
|
||||||
fn extract_derivation_from_text(text: &str) -> Option<Derivation> {
|
|
||||||
// Look for .drv path in text
|
|
||||||
if let Some(start) = text.find("/nix/store/") {
|
|
||||||
if let Some(end) = text[start..].find(".drv") {
|
|
||||||
let drv_path = &text[start..start + end + 4]; // Include .drv
|
|
||||||
return Derivation::parse(drv_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,6 @@ pub enum DisplayFormat {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log prefix style for build logs
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum LogPrefixStyle {
|
|
||||||
/// Just package name (pname)
|
|
||||||
Short,
|
|
||||||
/// Full derivation name with version
|
|
||||||
Full,
|
|
||||||
/// No prefix
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Summary display style
|
/// Summary display style
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum SummaryStyle {
|
pub enum SummaryStyle {
|
||||||
|
|
@ -45,18 +34,6 @@ impl SummaryStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogPrefixStyle {
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_str(s: &str) -> Self {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"short" => Self::Short,
|
|
||||||
"full" => Self::Full,
|
|
||||||
"none" => Self::None,
|
|
||||||
_ => Self::Short,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayFormat {
|
impl DisplayFormat {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_str(s: &str) -> Self {
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
|
@ -88,10 +65,6 @@ pub struct Config {
|
||||||
pub legend_style: String,
|
pub legend_style: String,
|
||||||
/// Summary display style
|
/// Summary display style
|
||||||
pub summary_style: String,
|
pub summary_style: String,
|
||||||
/// Log prefix style for build logs
|
|
||||||
pub log_prefix_style: LogPrefixStyle,
|
|
||||||
/// Maximum number of log lines to display (None = unlimited)
|
|
||||||
pub log_line_limit: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -105,8 +78,6 @@ impl Default for Config {
|
||||||
format: DisplayFormat::Tree,
|
format: DisplayFormat::Tree,
|
||||||
legend_style: "table".to_string(),
|
legend_style: "table".to_string(),
|
||||||
summary_style: "concise".to_string(),
|
summary_style: "concise".to_string(),
|
||||||
log_prefix_style: LogPrefixStyle::Short,
|
|
||||||
log_line_limit: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,8 +103,6 @@ mod tests {
|
||||||
assert_eq!(config.input_mode, InputMode::Human);
|
assert_eq!(config.input_mode, InputMode::Human);
|
||||||
assert!(config.show_timers);
|
assert!(config.show_timers);
|
||||||
assert_eq!(config.format, DisplayFormat::Tree);
|
assert_eq!(config.format, DisplayFormat::Tree);
|
||||||
assert_eq!(config.log_prefix_style, LogPrefixStyle::Short);
|
|
||||||
assert_eq!(config.log_line_limit, None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,11 @@
|
||||||
use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity};
|
use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::state::{
|
||||||
cache::BuildReportCache,
|
|
||||||
state::{
|
|
||||||
ActivityProgress,
|
ActivityProgress,
|
||||||
ActivityStatus,
|
ActivityStatus,
|
||||||
BuildFail,
|
BuildFail,
|
||||||
BuildInfo,
|
BuildInfo,
|
||||||
BuildReport,
|
|
||||||
BuildStatus,
|
BuildStatus,
|
||||||
CompletedBuildInfo,
|
CompletedBuildInfo,
|
||||||
CompletedTransferInfo,
|
CompletedTransferInfo,
|
||||||
|
|
@ -25,7 +22,6 @@ use crate::{
|
||||||
StorePathState,
|
StorePathState,
|
||||||
TransferInfo,
|
TransferInfo,
|
||||||
current_time,
|
current_time,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Process a nix JSON message and update state
|
/// Process a nix JSON message and update state
|
||||||
|
|
@ -100,19 +96,10 @@ fn handle_start(
|
||||||
109 => handle_query_path_info_start(state, id, &text, &fields, now), /* QueryPathInfo */
|
109 => handle_query_path_info_start(state, id, &text, &fields, now), /* QueryPathInfo */
|
||||||
110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */
|
110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */
|
||||||
101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */
|
101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */
|
||||||
100 => handle_copy_path_start(state, id, &text, &fields, now), /* CopyPath */
|
100 => handle_copy_path_start(state, id, &text, &fields, now), // CopyPath
|
||||||
104 => {
|
102 | 103 | 104 | 106 | 107 | 111 | 112 => {
|
||||||
// Builds activity - track this as the top-level builds activity
|
// Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting,
|
||||||
if state.builds_activity.is_none() {
|
// FetchTree These activities have no fields and are just tracked
|
||||||
state.builds_activity = Some(id);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
102 | 103 | 106 | 107 | 111 | 112 => {
|
|
||||||
// Realise, CopyPaths, OptimiseStore, VerifyPaths, BuildWaiting, FetchTree
|
|
||||||
// These activities have no fields and are just tracked
|
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -294,12 +281,11 @@ fn handle_result(
|
||||||
|
|
||||||
match result_type {
|
match result_type {
|
||||||
100 => {
|
100 => {
|
||||||
// FileLinked: 2 int fields (linked count, total count)
|
// FileLinked: 2 int fields
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2 {
|
||||||
let linked = fields[0].as_u64().unwrap_or(0);
|
let _linked = fields[0].as_u64();
|
||||||
let total = fields[1].as_u64().unwrap_or(0);
|
let _total = fields[1].as_u64();
|
||||||
debug!("FileLinked: {}/{}", linked, total);
|
// TODO: Track file linking progress
|
||||||
// File linking is reported but doesn't need state tracking
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -314,18 +300,17 @@ fn handle_result(
|
||||||
102 => {
|
102 => {
|
||||||
// UntrustedPath: 1 text field (store path)
|
// UntrustedPath: 1 text field (store path)
|
||||||
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
||||||
debug!("Untrusted path reported: {}", path_str);
|
debug!("Untrusted path: {}", path_str);
|
||||||
state
|
// TODO: Track untrusted paths
|
||||||
.nix_errors
|
|
||||||
.push(format!("Untrusted path: {}", path_str));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
103 => {
|
103 => {
|
||||||
// CorruptedPath: 1 text field (store path)
|
// CorruptedPath: 1 text field (store path)
|
||||||
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
||||||
state.nix_errors.push(format!("Corrupted path: {path_str}"));
|
state
|
||||||
|
.nix_errors
|
||||||
|
.push(format!("Corrupted path: {path_str}"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
@ -349,19 +334,6 @@ fn handle_result(
|
||||||
fields[2].as_u64(),
|
fields[2].as_u64(),
|
||||||
fields[3].as_u64(),
|
fields[3].as_u64(),
|
||||||
) {
|
) {
|
||||||
// If this progress is for the Builds activity, track success tokens
|
|
||||||
if state.builds_activity == Some(id) {
|
|
||||||
if let Some(activity) = state.activities.get(&id) {
|
|
||||||
if let Some(prev_progress) = &activity.progress {
|
|
||||||
let new_done = done.saturating_sub(prev_progress.done);
|
|
||||||
if new_done > 0 {
|
|
||||||
state.success_tokens =
|
|
||||||
state.success_tokens.saturating_add(new_done);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(activity) = state.activities.get_mut(&id) {
|
if let Some(activity) = state.activities.get_mut(&id) {
|
||||||
activity.progress = Some(ActivityProgress {
|
activity.progress = Some(ActivityProgress {
|
||||||
done,
|
done,
|
||||||
|
|
@ -378,13 +350,9 @@ fn handle_result(
|
||||||
106 => {
|
106 => {
|
||||||
// SetExpected: 2 int fields (activity type, count)
|
// SetExpected: 2 int fields (activity type, count)
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2 {
|
||||||
let activity_type = fields[0].as_u64().unwrap_or(0);
|
let _activity_type = fields[0].as_u64();
|
||||||
let expected_count = fields[1].as_u64().unwrap_or(0);
|
let _expected_count = fields[1].as_u64();
|
||||||
debug!(
|
// TODO: Track expected counts
|
||||||
"SetExpected: activity_type={}, count={}",
|
|
||||||
activity_type, expected_count
|
|
||||||
);
|
|
||||||
// Expected counts are informational and don't affect state tracking
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -400,7 +368,7 @@ fn handle_result(
|
||||||
// FetchStatus: 1 text field
|
// FetchStatus: 1 text field
|
||||||
if let Some(status) = fields.first().and_then(|f| f.as_str()) {
|
if let Some(status) = fields.first().and_then(|f| f.as_str()) {
|
||||||
debug!("Fetch status: {}", status);
|
debug!("Fetch status: {}", status);
|
||||||
// Fetch status is informational
|
// TODO: Track fetch status
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -411,50 +379,6 @@ fn handle_result(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get build time estimate from cache
|
|
||||||
fn get_build_estimate(
|
|
||||||
state: &State,
|
|
||||||
derivation_name: &str,
|
|
||||||
host: &Host,
|
|
||||||
) -> Option<u64> {
|
|
||||||
// Use pname if available, otherwise derivation name
|
|
||||||
let lookup_name = derivation_name.to_string();
|
|
||||||
let host_str = host.name();
|
|
||||||
|
|
||||||
BuildReportCache::calculate_median(
|
|
||||||
state
|
|
||||||
.build_cache
|
|
||||||
.get(&(host_str.to_string(), lookup_name))?
|
|
||||||
.as_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record completed build for future predictions
|
|
||||||
fn record_build_completion(
|
|
||||||
state: &mut State,
|
|
||||||
derivation_name: String,
|
|
||||||
platform: Option<String>,
|
|
||||||
start: f64,
|
|
||||||
end: f64,
|
|
||||||
host: &Host,
|
|
||||||
) {
|
|
||||||
let duration_secs = end - start;
|
|
||||||
let completed_at = std::time::SystemTime::now();
|
|
||||||
|
|
||||||
let report = BuildReport {
|
|
||||||
derivation_name: derivation_name.clone(),
|
|
||||||
platform: platform.unwrap_or_default(),
|
|
||||||
duration_secs,
|
|
||||||
completed_at,
|
|
||||||
host: host.name().to_string(),
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store in state for later CSV persistence
|
|
||||||
let key = (host.name().to_string(), derivation_name);
|
|
||||||
state.build_cache.entry(key).or_default().push(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_build_start(
|
fn handle_build_start(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
id: Id,
|
id: Id,
|
||||||
|
|
@ -478,16 +402,13 @@ fn handle_build_start(
|
||||||
if let Some(drv_path) = drv_path {
|
if let Some(drv_path) = drv_path {
|
||||||
debug!("Extracted derivation path: {}", drv_path);
|
debug!("Extracted derivation path: {}", drv_path);
|
||||||
if let Some(drv) = Derivation::parse(&drv_path) {
|
if let Some(drv) = Derivation::parse(&drv_path) {
|
||||||
let drv_id = state.get_or_create_derivation_id(drv.clone());
|
let drv_id = state.get_or_create_derivation_id(drv);
|
||||||
let host = extract_host(text);
|
let host = extract_host(text);
|
||||||
|
|
||||||
// Get build time estimate from cache
|
|
||||||
let estimate = get_build_estimate(state, &drv.name, &host);
|
|
||||||
|
|
||||||
let build_info = BuildInfo {
|
let build_info = BuildInfo {
|
||||||
start: now,
|
start: now,
|
||||||
host,
|
host,
|
||||||
estimate,
|
estimate: None,
|
||||||
activity_id: Some(id),
|
activity_id: Some(id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -523,42 +444,21 @@ fn handle_build_start(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_build_stop(state: &mut State, id: Id, now: f64) -> bool {
|
fn handle_build_stop(state: &mut State, id: Id, _now: f64) -> bool {
|
||||||
// Check if we have success tokens to consume
|
|
||||||
if state.success_tokens > 0 {
|
|
||||||
// Find the derivation associated with this activity
|
// Find the derivation associated with this activity
|
||||||
for (drv_id, info) in state.derivation_infos.clone().iter() {
|
for (drv_id, info) in &state.derivation_infos {
|
||||||
if let BuildStatus::Building(build_info) = &info.build_status {
|
match &info.build_status {
|
||||||
if build_info.activity_id == Some(id) {
|
BuildStatus::Building(build_info)
|
||||||
// Consume a success token and mark build as complete
|
if build_info.activity_id == Some(id) =>
|
||||||
state.success_tokens = state.success_tokens.saturating_sub(1);
|
{
|
||||||
state.update_build_status(*drv_id, BuildStatus::Built {
|
// Build was stopped but not marked as completed
|
||||||
info: build_info.clone(),
|
// It might be cancelled
|
||||||
end: now,
|
debug!("Build stopped for derivation {}", drv_id);
|
||||||
});
|
return false;
|
||||||
|
},
|
||||||
// Record build completion for future predictions
|
_ => {},
|
||||||
record_build_completion(
|
|
||||||
state,
|
|
||||||
info.name.name.clone(),
|
|
||||||
info.platform.clone(),
|
|
||||||
build_info.start,
|
|
||||||
now,
|
|
||||||
&build_info.host,
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Build completed for derivation {} (success_tokens: {})",
|
|
||||||
drv_id, state.success_tokens
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No success tokens - build was stopped without completion signal
|
|
||||||
debug!("Build stopped for activity {} without success token", id);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue