rom: bump dependencies; further work for feature parity
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I27d00ffe0d27497bdc6c1f890af3bdfe6a6a6964
This commit is contained in:
parent
2897c607c6
commit
27ecaac59c
11 changed files with 1274 additions and 337 deletions
588
Cargo.lock
generated
588
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.85"
|
rust-version = "1.91.1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
|
|
@ -19,6 +19,7 @@ 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"] }
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ 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
Normal file
397
rom/src/cache.rs
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
179
rom/src/cli.rs
179
rom/src/cli.rs
|
|
@ -33,6 +33,14 @@ 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)]
|
||||||
|
|
@ -86,6 +94,8 @@ 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(())
|
||||||
},
|
},
|
||||||
|
|
@ -101,6 +111,8 @@ 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(())
|
||||||
},
|
},
|
||||||
|
|
@ -110,14 +122,18 @@ pub fn run() -> eyre::Result<()> {
|
||||||
// If no args provided and --json is set, use piping mode from stdin
|
// If no args provided and --json is set, use piping mode from stdin
|
||||||
if args.is_empty() && cli.json {
|
if args.is_empty() && cli.json {
|
||||||
let config = crate::types::Config {
|
let config = crate::types::Config {
|
||||||
piping: false,
|
piping: false,
|
||||||
silent: cli.silent,
|
silent: cli.silent,
|
||||||
input_mode: crate::types::InputMode::Json,
|
input_mode: crate::types::InputMode::Json,
|
||||||
show_timers: true,
|
show_timers: true,
|
||||||
width: None,
|
width: None,
|
||||||
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();
|
||||||
|
|
@ -140,6 +156,8 @@ 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(())
|
||||||
},
|
},
|
||||||
|
|
@ -149,14 +167,18 @@ pub fn run() -> eyre::Result<()> {
|
||||||
// If no args provided and --json is set, use piping mode from stdin
|
// If no args provided and --json is set, use piping mode from stdin
|
||||||
if args.is_empty() && cli.json {
|
if args.is_empty() && cli.json {
|
||||||
let config = crate::types::Config {
|
let config = crate::types::Config {
|
||||||
piping: false,
|
piping: false,
|
||||||
silent: cli.silent,
|
silent: cli.silent,
|
||||||
input_mode: crate::types::InputMode::Json,
|
input_mode: crate::types::InputMode::Json,
|
||||||
show_timers: true,
|
show_timers: true,
|
||||||
width: None,
|
width: None,
|
||||||
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();
|
||||||
|
|
@ -179,6 +201,8 @@ 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(())
|
||||||
},
|
},
|
||||||
|
|
@ -188,14 +212,18 @@ pub fn run() -> eyre::Result<()> {
|
||||||
// If no args provided and --json is set, use piping mode from stdin
|
// If no args provided and --json is set, use piping mode from stdin
|
||||||
if args.is_empty() && cli.json {
|
if args.is_empty() && cli.json {
|
||||||
let config = crate::types::Config {
|
let config = crate::types::Config {
|
||||||
piping: false,
|
piping: false,
|
||||||
silent: cli.silent,
|
silent: cli.silent,
|
||||||
input_mode: crate::types::InputMode::Json,
|
input_mode: crate::types::InputMode::Json,
|
||||||
show_timers: true,
|
show_timers: true,
|
||||||
width: None,
|
width: None,
|
||||||
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();
|
||||||
|
|
@ -218,6 +246,8 @@ 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(())
|
||||||
},
|
},
|
||||||
|
|
@ -239,6 +269,10 @@ 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();
|
||||||
|
|
@ -280,6 +314,8 @@ 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() {
|
||||||
|
|
@ -310,6 +346,8 @@ 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);
|
||||||
|
|
@ -325,6 +363,8 @@ 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() {
|
||||||
|
|
@ -360,6 +400,8 @@ 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 {
|
||||||
|
|
@ -391,6 +433,8 @@ 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
|
||||||
|
|
@ -419,6 +463,8 @@ 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 {
|
||||||
|
|
@ -450,6 +496,8 @@ 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},
|
||||||
|
|
@ -481,6 +529,13 @@ 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;
|
||||||
|
|
@ -495,19 +550,62 @@ 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);
|
||||||
|
|
||||||
// Print messages immediately to stdout
|
// Process the action first to update state
|
||||||
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);
|
crate::update::process_message(&mut state, action.clone());
|
||||||
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: {} -> {}",
|
||||||
|
|
@ -518,9 +616,16 @@ fn run_monitored_command(
|
||||||
debug!("Failed to parse JSON: {}", json_line);
|
debug!("Failed to parse JSON: {}", json_line);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-JSON lines, pass through
|
// Non-JSON lines, buffer them
|
||||||
non_json_count += 1;
|
non_json_count += 1;
|
||||||
println!("{line}");
|
let mut logs = log_buffer_clone.lock().unwrap();
|
||||||
|
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!(
|
||||||
|
|
@ -583,13 +688,16 @@ 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, &[]);
|
let _ = display.render(&state, &logs);
|
||||||
} 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();
|
||||||
|
|
@ -599,8 +707,7 @@ 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) {
|
||||||
display.clear_previous().ok();
|
let _ = display.render(&state, &logs);
|
||||||
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 accumulated logs first (these go above the tree)
|
// Print build logs ABOVE the graph
|
||||||
for log in logs {
|
for log in logs {
|
||||||
lines.push(log.clone());
|
lines.push(log.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +153,8 @@ 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()?;
|
||||||
|
|
||||||
|
|
@ -180,6 +182,8 @@ 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}")?;
|
||||||
|
|
@ -207,8 +211,10 @@ 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),
|
||||||
|
|
@ -223,6 +229,18 @@ 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
|
||||||
|
|
@ -680,11 +698,23 @@ 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,
|
||||||
self.format_duration(elapsed)
|
time_info
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -954,8 +984,19 @@ impl<W: Write> Display<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time elapsed
|
// Time information
|
||||||
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,4 +1,5 @@
|
||||||
//! 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,12 +1,15 @@
|
||||||
//! 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::{
|
||||||
|
|
@ -54,7 +57,12 @@ impl<W: Write> Monitor<W> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let display = Display::new(writer, display_config)?;
|
let display = Display::new(writer, display_config)?;
|
||||||
let state = State::new();
|
let mut 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,
|
||||||
|
|
@ -90,6 +98,14 @@ 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);
|
||||||
|
|
@ -140,10 +156,6 @@ 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
|
||||||
|
|
@ -270,10 +282,8 @@ 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>() {
|
||||||
// XXX: This is a PlanCopies message, we'll probably track this
|
debug!("Copying {} paths", count);
|
||||||
// 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,6 +360,7 @@ 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>,
|
||||||
|
|
@ -371,6 +372,8 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
@ -390,6 +393,7 @@ 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(),
|
||||||
|
|
@ -401,6 +405,8 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
@ -697,6 +703,106 @@ 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,6 +11,17 @@ 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 {
|
||||||
|
|
@ -34,6 +45,18 @@ 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 {
|
||||||
|
|
@ -50,34 +73,40 @@ impl DisplayFormat {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Whether we're piping output through
|
/// Whether we're piping output through
|
||||||
pub piping: bool,
|
pub piping: bool,
|
||||||
/// Silent mode - minimal output
|
/// Silent mode - minimal output
|
||||||
pub silent: bool,
|
pub silent: bool,
|
||||||
/// Input parsing mode
|
/// Input parsing mode
|
||||||
pub input_mode: InputMode,
|
pub input_mode: InputMode,
|
||||||
/// Show completion times
|
/// Show completion times
|
||||||
pub show_timers: bool,
|
pub show_timers: bool,
|
||||||
/// Terminal width override
|
/// Terminal width override
|
||||||
pub width: Option<usize>,
|
pub width: Option<usize>,
|
||||||
/// Display format
|
/// Display format
|
||||||
pub format: DisplayFormat,
|
pub format: DisplayFormat,
|
||||||
/// Legend display style
|
/// Legend display style
|
||||||
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 {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
piping: false,
|
piping: false,
|
||||||
silent: false,
|
silent: false,
|
||||||
input_mode: InputMode::Human,
|
input_mode: InputMode::Human,
|
||||||
show_timers: true,
|
show_timers: true,
|
||||||
width: None,
|
width: None,
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +132,8 @@ 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,25 +3,29 @@
|
||||||
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::state::{
|
use crate::{
|
||||||
ActivityProgress,
|
cache::BuildReportCache,
|
||||||
ActivityStatus,
|
state::{
|
||||||
BuildFail,
|
ActivityProgress,
|
||||||
BuildInfo,
|
ActivityStatus,
|
||||||
BuildStatus,
|
BuildFail,
|
||||||
CompletedBuildInfo,
|
BuildInfo,
|
||||||
CompletedTransferInfo,
|
BuildReport,
|
||||||
Derivation,
|
BuildStatus,
|
||||||
DerivationId,
|
CompletedBuildInfo,
|
||||||
FailType,
|
CompletedTransferInfo,
|
||||||
FailedBuildInfo,
|
Derivation,
|
||||||
InputDerivation,
|
DerivationId,
|
||||||
State,
|
FailType,
|
||||||
StorePath,
|
FailedBuildInfo,
|
||||||
StorePathId,
|
InputDerivation,
|
||||||
StorePathState,
|
State,
|
||||||
TransferInfo,
|
StorePath,
|
||||||
current_time,
|
StorePathId,
|
||||||
|
StorePathState,
|
||||||
|
TransferInfo,
|
||||||
|
current_time,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Process a nix JSON message and update state
|
/// Process a nix JSON message and update state
|
||||||
|
|
@ -96,10 +100,19 @@ 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 */
|
||||||
102 | 103 | 104 | 106 | 107 | 111 | 112 => {
|
104 => {
|
||||||
// Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting,
|
// Builds activity - track this as the top-level builds activity
|
||||||
// FetchTree These activities have no fields and are just tracked
|
if state.builds_activity.is_none() {
|
||||||
|
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
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -281,11 +294,12 @@ fn handle_result(
|
||||||
|
|
||||||
match result_type {
|
match result_type {
|
||||||
100 => {
|
100 => {
|
||||||
// FileLinked: 2 int fields
|
// FileLinked: 2 int fields (linked count, total count)
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2 {
|
||||||
let _linked = fields[0].as_u64();
|
let linked = fields[0].as_u64().unwrap_or(0);
|
||||||
let _total = fields[1].as_u64();
|
let total = fields[1].as_u64().unwrap_or(0);
|
||||||
// TODO: Track file linking progress
|
debug!("FileLinked: {}/{}", linked, total);
|
||||||
|
// File linking is reported but doesn't need state tracking
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -300,17 +314,18 @@ 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: {}", path_str);
|
debug!("Untrusted path reported: {}", path_str);
|
||||||
// TODO: Track untrusted paths
|
state
|
||||||
|
.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
|
state.nix_errors.push(format!("Corrupted path: {path_str}"));
|
||||||
.nix_errors
|
|
||||||
.push(format!("Corrupted path: {path_str}"));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
@ -334,6 +349,19 @@ 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,
|
||||||
|
|
@ -350,9 +378,13 @@ 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();
|
let activity_type = fields[0].as_u64().unwrap_or(0);
|
||||||
let _expected_count = fields[1].as_u64();
|
let expected_count = fields[1].as_u64().unwrap_or(0);
|
||||||
// TODO: Track expected counts
|
debug!(
|
||||||
|
"SetExpected: activity_type={}, count={}",
|
||||||
|
activity_type, expected_count
|
||||||
|
);
|
||||||
|
// Expected counts are informational and don't affect state tracking
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -368,7 +400,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);
|
||||||
// TODO: Track fetch status
|
// Fetch status is informational
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
|
|
@ -379,6 +411,50 @@ 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,
|
||||||
|
|
@ -402,13 +478,16 @@ 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);
|
let drv_id = state.get_or_create_derivation_id(drv.clone());
|
||||||
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: None,
|
estimate,
|
||||||
activity_id: Some(id),
|
activity_id: Some(id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -444,21 +523,42 @@ 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 {
|
||||||
// Find the derivation associated with this activity
|
// Check if we have success tokens to consume
|
||||||
for (drv_id, info) in &state.derivation_infos {
|
if state.success_tokens > 0 {
|
||||||
match &info.build_status {
|
// Find the derivation associated with this activity
|
||||||
BuildStatus::Building(build_info)
|
for (drv_id, info) in state.derivation_infos.clone().iter() {
|
||||||
if build_info.activity_id == Some(id) =>
|
if let BuildStatus::Building(build_info) = &info.build_status {
|
||||||
{
|
if build_info.activity_id == Some(id) {
|
||||||
// Build was stopped but not marked as completed
|
// Consume a success token and mark build as complete
|
||||||
// It might be cancelled
|
state.success_tokens = state.success_tokens.saturating_sub(1);
|
||||||
debug!("Build stopped for derivation {}", drv_id);
|
state.update_build_status(*drv_id, BuildStatus::Built {
|
||||||
return false;
|
info: build_info.clone(),
|
||||||
},
|
end: now,
|
||||||
_ => {},
|
});
|
||||||
|
|
||||||
|
// 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