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"
|
||||
authors = ["NotAShelf <raf@notashelf.dev>"]
|
||||
description = "Pretty build graphs for Nix builds"
|
||||
rust-version = "1.91.1"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
|
|
@ -19,7 +19,6 @@ crossterm = "0.29.0"
|
|||
ratatui = "0.29.0"
|
||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||
csv = "1.4.0"
|
||||
chrono = "0.4.42"
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
|
|
|
|||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770115704,
|
||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
||||
"lastModified": 1765186076,
|
||||
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
||||
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ crossterm = "0.29"
|
|||
ratatui = "0.29"
|
||||
indexmap.workspace = true
|
||||
csv.workspace = true
|
||||
chrono.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.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
|
||||
}
|
||||
}
|
||||
179
rom/src/cli.rs
179
rom/src/cli.rs
|
|
@ -33,14 +33,6 @@ pub struct Cli {
|
|||
/// Summary display style: concise, table, full
|
||||
#[arg(long, global = true, default_value = "concise")]
|
||||
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)]
|
||||
|
|
@ -94,8 +86,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -111,8 +101,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -122,18 +110,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -156,8 +140,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -167,18 +149,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -201,8 +179,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -212,18 +188,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -246,8 +218,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -269,10 +239,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.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();
|
||||
|
|
@ -314,8 +280,6 @@ fn run_nix_build_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified
|
||||
if package_and_rom_args.is_empty() {
|
||||
|
|
@ -346,8 +310,6 @@ fn run_nix_build_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
if exit_code != 0 {
|
||||
std::process::exit(exit_code);
|
||||
|
|
@ -363,8 +325,6 @@ fn run_nix_shell_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified
|
||||
if package_and_rom_args.is_empty() {
|
||||
|
|
@ -400,8 +360,6 @@ fn run_nix_shell_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
|
||||
if exit_code != 0 {
|
||||
|
|
@ -433,8 +391,6 @@ fn run_nix_develop_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified (can be empty for
|
||||
// current flake) develop without args is valid (uses current directory's
|
||||
|
|
@ -463,8 +419,6 @@ fn run_nix_develop_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
|
||||
if exit_code != 0 {
|
||||
|
|
@ -496,8 +450,6 @@ fn run_monitored_command(
|
|||
format_str: String,
|
||||
legend_style_str: String,
|
||||
summary_style_str: String,
|
||||
log_prefix_style: crate::types::LogPrefixStyle,
|
||||
log_line_limit: Option<usize>,
|
||||
) -> eyre::Result<i32> {
|
||||
use std::{
|
||||
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_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)
|
||||
let stderr_thread = thread::spawn(move || {
|
||||
use tracing::debug;
|
||||
|
|
@ -550,62 +495,19 @@ fn run_monitored_command(
|
|||
if let Ok(action) = serde_json::from_str::<cognos::Actions>(json_line) {
|
||||
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 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(
|
||||
&mut state,
|
||||
crate::state::current_time(),
|
||||
);
|
||||
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 {
|
||||
debug!(
|
||||
"Derivation count changed: {} -> {}",
|
||||
|
|
@ -616,16 +518,9 @@ fn run_monitored_command(
|
|||
debug!("Failed to parse JSON: {}", json_line);
|
||||
}
|
||||
} else {
|
||||
// Non-JSON lines, buffer them
|
||||
// Non-JSON lines, pass through
|
||||
non_json_count += 1;
|
||||
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();
|
||||
}
|
||||
}
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
|
|
@ -688,16 +583,13 @@ fn run_monitored_command(
|
|||
|| !state.full_summary.planned_builds.is_empty();
|
||||
|
||||
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 {
|
||||
// Clear any previous timer display
|
||||
if last_timer_display.is_some() {
|
||||
display.clear_previous().ok();
|
||||
last_timer_display = None;
|
||||
}
|
||||
let _ = display.render(&state, &logs);
|
||||
let _ = display.render(&state, &[]);
|
||||
} else {
|
||||
// Show initial timer while waiting for activity
|
||||
let start = *start_time_clone.lock().unwrap();
|
||||
|
|
@ -707,7 +599,8 @@ fn run_monitored_command(
|
|||
|
||||
// Only update if changed (to avoid flicker)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// Print build logs ABOVE the graph
|
||||
// Print accumulated logs first (these go above the tree)
|
||||
for log in logs {
|
||||
lines.push(log.clone());
|
||||
}
|
||||
|
|
@ -153,8 +153,6 @@ impl<W: Write> Display<W> {
|
|||
}
|
||||
|
||||
pub fn render_final(&mut self, state: &State) -> io::Result<()> {
|
||||
tracing::debug!("render_final called");
|
||||
|
||||
// Clear any previous render
|
||||
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)
|
||||
for line in lines {
|
||||
writeln!(self.writer, "{line}")?;
|
||||
|
|
@ -211,10 +207,8 @@ impl<W: Write> Display<W> {
|
|||
let failed = state.full_summary.failed_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 {
|
||||
let duration = current_time() - state.start_time;
|
||||
lines.push(format!(
|
||||
"{} {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
|
||||
self.colored("━", Color::Blue),
|
||||
|
|
@ -229,18 +223,6 @@ impl<W: Write> Display<W> {
|
|||
self.colored("⏱", Color::Grey),
|
||||
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
|
||||
|
|
@ -698,23 +680,11 @@ impl<W: Write> Display<W> {
|
|||
if let Some(info) = state.get_derivation_info(*drv_id) {
|
||||
let name = &info.name.name;
|
||||
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!(
|
||||
" {} {} {}",
|
||||
self.colored("⏵", Color::Yellow),
|
||||
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;
|
||||
|
||||
// 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(
|
||||
&format!(" ⏱ {}", self.format_duration(elapsed)),
|
||||
Color::DarkGrey,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! ROM - Rust Output Monitor
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
//! Monitor module for orchestrating state updates and display rendering
|
||||
|
||||
use std::{
|
||||
io::{BufRead, Write},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cognos::Host;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
cache::BuildReportCache,
|
||||
display::{Display, DisplayConfig, LegendStyle, SummaryStyle},
|
||||
error::{Result, RomError},
|
||||
state::{
|
||||
|
|
@ -57,12 +54,7 @@ impl<W: Write> Monitor<W> {
|
|||
};
|
||||
|
||||
let display = Display::new(writer, display_config)?;
|
||||
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();
|
||||
let state = State::new();
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
|
|
@ -98,14 +90,6 @@ impl<W: Write> Monitor<W> {
|
|||
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
|
||||
if self.state.has_errors() {
|
||||
return Err(RomError::BuildFailed);
|
||||
|
|
@ -156,6 +140,10 @@ impl<W: Write> Monitor<W> {
|
|||
|
||||
/// Process a human-readable line
|
||||
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();
|
||||
|
||||
// Skip empty lines
|
||||
|
|
@ -282,8 +270,10 @@ impl<W: Write> Monitor<W> {
|
|||
// Extract number of paths if present
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
if words.len() >= 2 {
|
||||
if let Ok(count) = words[1].parse::<usize>() {
|
||||
debug!("Copying {} paths", count);
|
||||
if let Ok(_count) = words[1].parse::<usize>() {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
rom/src/state.rs
106
rom/src/state.rs
|
|
@ -360,7 +360,6 @@ pub struct State {
|
|||
pub full_summary: DependencySummary,
|
||||
pub forest_roots: Vec<DerivationId>,
|
||||
pub build_reports: HashMap<String, Vec<BuildReport>>,
|
||||
pub build_cache: HashMap<(String, String), Vec<BuildReport>>,
|
||||
pub start_time: f64,
|
||||
pub progress_state: ProgressState,
|
||||
pub store_path_ids: HashMap<StorePath, StorePathId>,
|
||||
|
|
@ -372,8 +371,6 @@ pub struct State {
|
|||
pub traces: Vec<String>,
|
||||
pub build_platform: Option<String>,
|
||||
pub evaluation_state: EvalInfo,
|
||||
pub builds_activity: Option<ActivityId>,
|
||||
pub success_tokens: u64,
|
||||
next_store_path_id: StorePathId,
|
||||
next_derivation_id: DerivationId,
|
||||
}
|
||||
|
|
@ -393,7 +390,6 @@ impl State {
|
|||
full_summary: DependencySummary::default(),
|
||||
forest_roots: Vec::new(),
|
||||
build_reports: HashMap::new(),
|
||||
build_cache: HashMap::new(),
|
||||
start_time: current_time(),
|
||||
progress_state: ProgressState::JustStarted,
|
||||
store_path_ids: HashMap::new(),
|
||||
|
|
@ -405,8 +401,6 @@ impl State {
|
|||
traces: Vec::new(),
|
||||
build_platform: None,
|
||||
evaluation_state: EvalInfo::default(),
|
||||
builds_activity: None,
|
||||
success_tokens: 0,
|
||||
next_store_path_id: 0,
|
||||
next_derivation_id: 0,
|
||||
}
|
||||
|
|
@ -703,106 +697,6 @@ impl State {
|
|||
.copied()
|
||||
.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]
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@ pub enum DisplayFormat {
|
|||
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
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
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 {
|
||||
#[must_use]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
|
|
@ -73,40 +50,34 @@ impl DisplayFormat {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Whether we're piping output through
|
||||
pub piping: bool,
|
||||
pub piping: bool,
|
||||
/// Silent mode - minimal output
|
||||
pub silent: bool,
|
||||
pub silent: bool,
|
||||
/// Input parsing mode
|
||||
pub input_mode: InputMode,
|
||||
pub input_mode: InputMode,
|
||||
/// Show completion times
|
||||
pub show_timers: bool,
|
||||
pub show_timers: bool,
|
||||
/// Terminal width override
|
||||
pub width: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
/// Display format
|
||||
pub format: DisplayFormat,
|
||||
pub format: DisplayFormat,
|
||||
/// Legend display style
|
||||
pub legend_style: String,
|
||||
pub legend_style: String,
|
||||
/// Summary display style
|
||||
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>,
|
||||
pub summary_style: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
piping: false,
|
||||
silent: false,
|
||||
input_mode: InputMode::Human,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: DisplayFormat::Tree,
|
||||
legend_style: "table".to_string(),
|
||||
summary_style: "concise".to_string(),
|
||||
log_prefix_style: LogPrefixStyle::Short,
|
||||
log_line_limit: None,
|
||||
piping: false,
|
||||
silent: false,
|
||||
input_mode: InputMode::Human,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: DisplayFormat::Tree,
|
||||
legend_style: "table".to_string(),
|
||||
summary_style: "concise".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,8 +103,6 @@ mod tests {
|
|||
assert_eq!(config.input_mode, InputMode::Human);
|
||||
assert!(config.show_timers);
|
||||
assert_eq!(config.format, DisplayFormat::Tree);
|
||||
assert_eq!(config.log_prefix_style, LogPrefixStyle::Short);
|
||||
assert_eq!(config.log_line_limit, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -3,29 +3,25 @@
|
|||
use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
cache::BuildReportCache,
|
||||
state::{
|
||||
ActivityProgress,
|
||||
ActivityStatus,
|
||||
BuildFail,
|
||||
BuildInfo,
|
||||
BuildReport,
|
||||
BuildStatus,
|
||||
CompletedBuildInfo,
|
||||
CompletedTransferInfo,
|
||||
Derivation,
|
||||
DerivationId,
|
||||
FailType,
|
||||
FailedBuildInfo,
|
||||
InputDerivation,
|
||||
State,
|
||||
StorePath,
|
||||
StorePathId,
|
||||
StorePathState,
|
||||
TransferInfo,
|
||||
current_time,
|
||||
},
|
||||
use crate::state::{
|
||||
ActivityProgress,
|
||||
ActivityStatus,
|
||||
BuildFail,
|
||||
BuildInfo,
|
||||
BuildStatus,
|
||||
CompletedBuildInfo,
|
||||
CompletedTransferInfo,
|
||||
Derivation,
|
||||
DerivationId,
|
||||
FailType,
|
||||
FailedBuildInfo,
|
||||
InputDerivation,
|
||||
State,
|
||||
StorePath,
|
||||
StorePathId,
|
||||
StorePathState,
|
||||
TransferInfo,
|
||||
current_time,
|
||||
};
|
||||
|
||||
/// 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 */
|
||||
110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */
|
||||
101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */
|
||||
100 => handle_copy_path_start(state, id, &text, &fields, now), /* CopyPath */
|
||||
104 => {
|
||||
// Builds activity - track this as the top-level builds activity
|
||||
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
|
||||
100 => handle_copy_path_start(state, id, &text, &fields, now), // CopyPath
|
||||
102 | 103 | 104 | 106 | 107 | 111 | 112 => {
|
||||
// Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting,
|
||||
// FetchTree These activities have no fields and are just tracked
|
||||
true
|
||||
},
|
||||
_ => {
|
||||
|
|
@ -294,12 +281,11 @@ fn handle_result(
|
|||
|
||||
match result_type {
|
||||
100 => {
|
||||
// FileLinked: 2 int fields (linked count, total count)
|
||||
// FileLinked: 2 int fields
|
||||
if fields.len() >= 2 {
|
||||
let linked = fields[0].as_u64().unwrap_or(0);
|
||||
let total = fields[1].as_u64().unwrap_or(0);
|
||||
debug!("FileLinked: {}/{}", linked, total);
|
||||
// File linking is reported but doesn't need state tracking
|
||||
let _linked = fields[0].as_u64();
|
||||
let _total = fields[1].as_u64();
|
||||
// TODO: Track file linking progress
|
||||
}
|
||||
false
|
||||
},
|
||||
|
|
@ -314,18 +300,17 @@ fn handle_result(
|
|||
102 => {
|
||||
// UntrustedPath: 1 text field (store path)
|
||||
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
||||
debug!("Untrusted path reported: {}", path_str);
|
||||
state
|
||||
.nix_errors
|
||||
.push(format!("Untrusted path: {}", path_str));
|
||||
return true;
|
||||
debug!("Untrusted path: {}", path_str);
|
||||
// TODO: Track untrusted paths
|
||||
}
|
||||
false
|
||||
},
|
||||
103 => {
|
||||
// CorruptedPath: 1 text field (store path)
|
||||
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;
|
||||
}
|
||||
false
|
||||
|
|
@ -349,19 +334,6 @@ fn handle_result(
|
|||
fields[2].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) {
|
||||
activity.progress = Some(ActivityProgress {
|
||||
done,
|
||||
|
|
@ -378,13 +350,9 @@ fn handle_result(
|
|||
106 => {
|
||||
// SetExpected: 2 int fields (activity type, count)
|
||||
if fields.len() >= 2 {
|
||||
let activity_type = fields[0].as_u64().unwrap_or(0);
|
||||
let expected_count = fields[1].as_u64().unwrap_or(0);
|
||||
debug!(
|
||||
"SetExpected: activity_type={}, count={}",
|
||||
activity_type, expected_count
|
||||
);
|
||||
// Expected counts are informational and don't affect state tracking
|
||||
let _activity_type = fields[0].as_u64();
|
||||
let _expected_count = fields[1].as_u64();
|
||||
// TODO: Track expected counts
|
||||
}
|
||||
false
|
||||
},
|
||||
|
|
@ -400,7 +368,7 @@ fn handle_result(
|
|||
// FetchStatus: 1 text field
|
||||
if let Some(status) = fields.first().and_then(|f| f.as_str()) {
|
||||
debug!("Fetch status: {}", status);
|
||||
// Fetch status is informational
|
||||
// TODO: Track fetch status
|
||||
}
|
||||
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(
|
||||
state: &mut State,
|
||||
id: Id,
|
||||
|
|
@ -478,16 +402,13 @@ fn handle_build_start(
|
|||
if let Some(drv_path) = drv_path {
|
||||
debug!("Extracted derivation path: {}", 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);
|
||||
|
||||
// Get build time estimate from cache
|
||||
let estimate = get_build_estimate(state, &drv.name, &host);
|
||||
|
||||
let build_info = BuildInfo {
|
||||
start: now,
|
||||
host,
|
||||
estimate,
|
||||
estimate: None,
|
||||
activity_id: Some(id),
|
||||
};
|
||||
|
||||
|
|
@ -523,42 +444,21 @@ fn handle_build_start(
|
|||
false
|
||||
}
|
||||
|
||||
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
|
||||
for (drv_id, info) in state.derivation_infos.clone().iter() {
|
||||
if let BuildStatus::Building(build_info) = &info.build_status {
|
||||
if build_info.activity_id == Some(id) {
|
||||
// Consume a success token and mark build as complete
|
||||
state.success_tokens = state.success_tokens.saturating_sub(1);
|
||||
state.update_build_status(*drv_id, BuildStatus::Built {
|
||||
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;
|
||||
}
|
||||
}
|
||||
fn handle_build_stop(state: &mut State, id: Id, _now: f64) -> bool {
|
||||
// Find the derivation associated with this activity
|
||||
for (drv_id, info) in &state.derivation_infos {
|
||||
match &info.build_status {
|
||||
BuildStatus::Building(build_info)
|
||||
if build_info.activity_id == Some(id) =>
|
||||
{
|
||||
// Build was stopped but not marked as completed
|
||||
// It might be cancelled
|
||||
debug!("Build stopped for derivation {}", drv_id);
|
||||
return false;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// No success tokens - build was stopped without completion signal
|
||||
debug!("Build stopped for activity {} without success token", id);
|
||||
false
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue