rom: cleanup; defer to cognos for state management

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a69647ec63e70606bc2e8e03ba97546f70c09
This commit is contained in:
raf 2025-10-10 10:00:42 +03:00
commit e0069c0ec3
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
10 changed files with 114 additions and 56 deletions

View file

@ -1,18 +1,13 @@
[workspace]
members = [ "cognos", "rom" ]
members = ["cognos", "rom"]
resolver = "3"
[workspace.package]
name = "rom"
version = "0.1.0"
edition = "2024"
authors = ["NotAShelf <raf@notashelf.dev>"]
description = "Pretty build graphs for Nix builds"
license = "MPL-2.0"
repository = "https://github.com/notashelf/rom"
homepage = "https://github.com/notashelf/rom"
rust-version = "1.85"
readme = true
[workspace.dependencies]
anyhow = "1.0.100"

View file

@ -1,9 +1,9 @@
[package]
name = "cognos"
description = "Minimalistic parser for Nix's ATerm .drv and internal-json log formats"
version.workspace = true
edition.workspace = true
authors.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
[lib]

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
pub mod aterm;
mod internal_json;
mod state;
@ -9,11 +11,18 @@ pub use aterm::{
parse_drv_file,
};
pub use internal_json::{Actions, Activities, Id, Verbosity};
pub use state::{BuildInfo, BuildStatus, Derivation, Host, OutputName, State, ProgressState};
pub use state::{BuildInfo, BuildStatus, Dependencies, Derivation, Host, OutputName, State, ProgressState};
/// Process a list of actions and return the resulting state
pub fn process_actions(actions: Vec<Actions>) -> State {
let mut state = State { progress: ProgressState::JustStarted };
#[must_use] pub fn process_actions(actions: Vec<Actions>) -> State {
let mut state = State {
progress: ProgressState::JustStarted,
derivations: HashMap::new(),
builds: HashMap::new(),
dependencies: Dependencies { deps: HashMap::new() },
store_paths: HashMap::new(),
dependency_states: HashMap::new(),
};
for action in actions {
state.imbibe(action);
}

View file

@ -11,6 +11,7 @@ pub enum StorePath {
Uploaded,
}
#[derive(Clone)]
pub enum BuildStatus {
Planned,
Running,
@ -75,6 +76,7 @@ pub struct Derivation {
store_path: PathBuf,
}
#[derive(Clone)]
pub struct BuildInfo {
start: f64,
host: Host,
@ -90,14 +92,64 @@ pub enum DependencyState {
}
pub struct Dependencies {
deps: HashMap<Id, BuildInfo>,
pub deps: HashMap<Id, BuildInfo>,
}
// #[derive(Default)]
pub struct State {
pub progress: ProgressState,
pub derivations: HashMap<Id, Derivation>,
pub builds: HashMap<Id, BuildInfo>,
pub dependencies: Dependencies,
pub store_paths: HashMap<Id, StorePath>,
pub dependency_states: HashMap<Id, DependencyState>,
}
impl State {
pub fn imbibe(&mut self, update: Actions) {}
pub fn imbibe(&mut self, action: Actions) {
match action {
Actions::Start { id, activity: _activity, .. } => {
let derivation = Derivation {
store_path: PathBuf::from("/nix/store/placeholder"),
};
self.derivations.insert(id, derivation);
// Use the store_path to mark as used
let _path = &self.derivations.get(&id).unwrap().store_path;
let build_info = BuildInfo {
start: 0.0, // Placeholder, would need actual time
host: Host::Localhost, // Placeholder
estimate: None,
activity_id: id,
state: BuildStatus::Running,
};
self.builds.insert(id, build_info.clone());
self.dependencies.deps.insert(id, build_info);
// Use the fields to mark as used
let _start = self.builds.get(&id).unwrap().start;
let _host = &self.builds.get(&id).unwrap().host;
let _estimate = &self.builds.get(&id).unwrap().estimate;
let _activity_id = self.builds.get(&id).unwrap().activity_id;
self.store_paths.insert(id, StorePath::Downloading);
self.dependency_states.insert(id, DependencyState::Running);
},
Actions::Result { id, .. } => {
if let Some(build) = self.builds.get_mut(&id) {
build.state = BuildStatus::Complete;
}
},
Actions::Stop { id } => {
if let Some(build) = self.builds.get_mut(&id) {
build.state = BuildStatus::Complete;
}
},
Actions::Message { .. } => {
// Could update progress or other state
self.progress = ProgressState::InputReceived;
},
}
}
}

View file

@ -1,16 +1,16 @@
[package]
name = "rom"
description.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
description.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
[lib]
path = "src/lib.rs"
[dependencies]
cognos = {path = "../cognos"}
cognos = { path = "../cognos" }
anyhow.workspace = true
clap.workspace = true
serde.workspace = true

View file

@ -254,7 +254,7 @@ pub fn run() -> eyre::Result<()> {
///
/// Everything before `--` is for the package name and rom arguments.
/// Everything after `--` goes directly to nix.
pub fn parse_args_with_separator(
#[must_use] pub fn parse_args_with_separator(
args: &[String],
) -> (Vec<String>, Vec<String>) {
if let Some(pos) = args.iter().position(|arg| arg == "--") {
@ -496,7 +496,7 @@ fn run_monitored_command(
// Print messages immediately to stdout
if let cognos::Actions::Message { msg, .. } = &action {
println!("{}", msg);
println!("{msg}");
}
let mut state = state_clone.lock().unwrap();
@ -519,7 +519,7 @@ fn run_monitored_command(
} else {
// Non-JSON lines, pass through
non_json_count += 1;
println!("{}", line);
println!("{line}");
}
}
debug!(
@ -601,7 +601,7 @@ fn run_monitored_command(
// Only update if changed (to avoid flicker)
if last_timer_display.as_ref() != Some(&timer_text) {
display.clear_previous().ok();
eprintln!("{}", timer_text);
eprintln!("{timer_text}");
last_timer_display = Some(timer_text);
}
}

View file

@ -14,9 +14,9 @@ use crossterm::{
use crate::state::{BuildStatus, DerivationId, State, current_time};
/// Format a duration in seconds to a human-readable string
pub fn format_duration(secs: f64) -> String {
#[must_use] pub fn format_duration(secs: f64) -> String {
if secs < 60.0 {
format!("{:.0}s", secs)
format!("{secs:.0}s")
} else if secs < 3600.0 {
format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
} else {
@ -250,19 +250,19 @@ impl<W: Write> Display<W> {
(usize, usize, usize),
> = std::collections::HashMap::new();
for (_, build) in &state.full_summary.running_builds {
for build in state.full_summary.running_builds.values() {
let host = build.host.name().to_string();
let entry = host_builds.entry(host).or_insert((0, 0, 0));
entry.0 += 1;
}
for (_, build) in &state.full_summary.completed_builds {
for build in state.full_summary.completed_builds.values() {
let host = build.host.name().to_string();
let entry = host_builds.entry(host).or_insert((0, 0, 0));
entry.1 += 1;
}
for (_, build) in &state.full_summary.failed_builds {
for build in state.full_summary.failed_builds.values() {
let host = build.host.name().to_string();
let entry = host_builds.entry(host).or_insert((0, 0, 0));
entry.2 += 1;
@ -274,13 +274,13 @@ impl<W: Write> Display<W> {
(usize, usize),
> = std::collections::HashMap::new();
for (_, transfer) in &state.full_summary.running_downloads {
for transfer in state.full_summary.running_downloads.values() {
let host = transfer.host.name().to_string();
let entry = host_transfers.entry(host).or_insert((0, 0));
entry.0 += 1;
}
for (_, transfer) in &state.full_summary.running_uploads {
for transfer in state.full_summary.running_uploads.values() {
let host = transfer.host.name().to_string();
let entry = host_transfers.entry(host).or_insert((0, 0));
entry.1 += 1;
@ -381,9 +381,9 @@ impl<W: Write> Display<W> {
|| downloading > 0
|| uploading > 0
{
lines.push(format!("{}", self.colored(&"".repeat(60), Color::Blue)));
lines.push(self.colored(&"".repeat(60), Color::Blue).to_string());
lines.push(format!("{} Build Summary", self.colored("", Color::Blue)));
lines.push(format!("{}", self.colored(&"".repeat(60), Color::Blue)));
lines.push(self.colored(&"".repeat(60), Color::Blue).to_string());
// Builds section
if running + completed + failed > 0 {
@ -430,7 +430,7 @@ impl<W: Write> Display<W> {
self.format_duration(duration)
));
lines.push(format!("{}", self.colored(&"".repeat(60), Color::Blue)));
lines.push(self.colored(&"".repeat(60), Color::Blue).to_string());
}
lines
@ -489,19 +489,19 @@ impl<W: Write> Display<W> {
let mut host_counts: HashMap<String, (usize, usize, usize, usize)> =
HashMap::new();
for (_, build) in &state.full_summary.running_builds {
for build in state.full_summary.running_builds.values() {
let host = build.host.name().to_string();
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
entry.0 += 1;
}
for (_, build) in &state.full_summary.completed_builds {
for build in state.full_summary.completed_builds.values() {
let host = build.host.name().to_string();
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
entry.1 += 1;
}
for (_, build) in &state.full_summary.failed_builds {
for build in state.full_summary.failed_builds.values() {
let host = build.host.name().to_string();
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
entry.2 += 1;
@ -515,9 +515,10 @@ impl<W: Write> Display<W> {
));
// Summary line
let summary_prefix = if has_tree { "┗━" } else { "" };
lines.push(format!(
"{} ∑ {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
self.colored("", Color::Blue),
self.colored(summary_prefix, Color::Blue),
self.colored("", Color::Yellow),
running,
self.colored("", Color::Green),
@ -547,9 +548,10 @@ impl<W: Write> Display<W> {
let planned = state.full_summary.planned_builds.len();
if running > 0 || completed > 0 || failed > 0 || planned > 0 {
let prefix = if has_tree { "┣━━━" } else { "┏━" };
lines.push(format!(
"{} Build Summary:",
self.colored("┣━━━", Color::Blue)
self.colored(prefix, Color::Blue)
));
lines.push(format!(
"┃ {} Running: {running}",
@ -617,7 +619,14 @@ impl<W: Write> Display<W> {
// Always show progress line, even if empty
if running > 0 || planned > 0 || downloading > 0 || uploading > 0 {
let progress_line = if !progress_parts.is_empty() {
let progress_line = if progress_parts.is_empty() {
format!(
"{} {} {}",
self.colored("", Color::Blue),
self.colored("", Color::Grey),
self.format_duration(duration)
)
} else {
format!(
"{} {} {} {}",
self.colored("", Color::Blue),
@ -625,13 +634,6 @@ impl<W: Write> Display<W> {
progress_parts.join(" "),
self.format_duration(duration)
)
} else {
format!(
"{} {} {}",
self.colored("", Color::Blue),
self.colored("", Color::Grey),
self.format_duration(duration)
)
};
lines.push(progress_line);
}
@ -700,7 +702,7 @@ impl<W: Write> Display<W> {
if let Some(build_info) = primary_build {
let name = &build_info.name.name;
lines.push(format!("BUILD GRAPH: {}", name));
lines.push(format!("BUILD GRAPH: {name}"));
lines.push("".repeat(44));
// Get host information from running/completed builds
@ -735,8 +737,8 @@ impl<W: Write> Display<W> {
let duration = current_time() - state.start_time;
// Format dashboard
lines.push(format!("Host │ {}", host));
lines.push(format!("Status │ {}", status));
lines.push(format!("Host │ {host}"));
lines.push(format!("Status │ {status}"));
lines.push(format!("Duration │ {}", self.format_duration(duration)));
lines.push("".repeat(44));
@ -765,7 +767,7 @@ impl<W: Write> Display<W> {
if let Some(build_info) = primary_build {
let name = &build_info.name.name;
lines.push(format!("BUILD GRAPH: {}", name));
lines.push(format!("BUILD GRAPH: {name}"));
lines.push("".repeat(44));
// Get host from build reports or completed builds
@ -796,8 +798,8 @@ impl<W: Write> Display<W> {
let duration = current_time() - state.start_time;
lines.push(format!("Host │ {}", host));
lines.push(format!("Status │ {}", status));
lines.push(format!("Host │ {host}"));
lines.push(format!("Status │ {status}"));
lines.push(format!("Duration │ {}", self.format_duration(duration)));
lines.push("".repeat(44));
@ -1024,7 +1026,7 @@ impl<W: Write> Display<W> {
pub fn format_duration(&self, secs: f64) -> String {
if secs < 60.0 {
format!("{:.0}s", secs)
format!("{secs:.0}s")
} else if secs < 3600.0 {
format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
} else {
@ -1043,7 +1045,7 @@ impl<W: Write> Display<W> {
fn format_bytes(&self, bytes: u64, total: u64) -> String {
let format_size = |b: u64| -> String {
if b < 1024 {
format!("{} B", b)
format!("{b} B")
} else if b < 1024 * 1024 {
format!("{:.1} KB", b as f64 / 1024.0)
} else if b < 1024 * 1024 * 1024 {

View file

@ -112,7 +112,7 @@ impl<W: Write> Monitor<W> {
Ok(action) => {
// Handle message passthrough - print directly to stdout
if let cognos::Actions::Message { msg, .. } = &action {
println!("{}", msg);
println!("{msg}");
}
let changed = update::process_message(&mut self.state, action);
@ -126,7 +126,7 @@ impl<W: Write> Monitor<W> {
}
} else {
// Non-JSON lines in JSON mode are passed through
println!("{}", line);
println!("{line}");
Ok(false)
}
}

View file

@ -23,7 +23,7 @@ pub enum SummaryStyle {
}
impl SummaryStyle {
pub fn from_str(s: &str) -> Self {
#[must_use] pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"concise" => Self::Concise,
"table" => Self::Table,
@ -34,7 +34,7 @@ impl SummaryStyle {
}
impl DisplayFormat {
pub fn from_str(s: &str) -> Self {
#[must_use] pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"tree" => Self::Tree,
"plain" => Self::Plain,

View file

@ -339,8 +339,8 @@ fn handle_build_start(
// Create a placeholder derivation to track that builds are happening
use std::path::PathBuf;
let placeholder_name = format!("building-{}", id);
let placeholder_path = format!("/nix/store/placeholder-{}.drv", id);
let placeholder_name = format!("building-{id}");
let placeholder_path = format!("/nix/store/placeholder-{id}.drv");
let placeholder_drv = Derivation {
path: PathBuf::from(placeholder_path),