1use std::{
3 io,
4 path::PathBuf,
5 process::{Command, Stdio},
6};
7
8use clap::Parser;
9use cognos::ProgressState;
10use tracing_subscriber::EnvFilter;
11
12#[derive(Debug, Parser)]
13#[command(name = "rom", version, about = "ROM - A Nix build output monitor")]
14pub struct Cli {
15 #[command(subcommand)]
16 pub command: Option<Commands>,
17
18 #[arg(long, global = true)]
20 pub json: bool,
21
22 #[arg(long, global = true)]
24 pub silent: bool,
25
26 #[arg(long, global = true, default_value = "tree")]
28 pub format: String,
29
30 #[arg(long, global = true, default_value = "table")]
32 pub legend: String,
33
34 #[arg(long, global = true, default_value = "concise")]
36 pub summary: String,
37
38 #[arg(long, global = true, default_value = "short")]
40 pub log_prefix: String,
41
42 #[arg(long, global = true)]
44 pub log_lines: Option<usize>,
45
46 #[arg(long, global = true)]
48 pub platform: Option<String>,
49
50 #[arg(short = 'v', action = clap::ArgAction::Count, global = true)]
53 pub verbose: u8,
54}
55
56#[derive(Debug, clap::Subcommand)]
57pub enum Commands {
58 Build {
60 packages: Vec<String>,
62
63 #[arg(last = true)]
65 nix_flags: Vec<String>,
66 },
67
68 Shell {
70 packages: Vec<String>,
72
73 #[arg(last = true)]
75 nix_flags: Vec<String>,
76 },
77
78 Develop {
80 packages: Vec<String>,
82
83 #[arg(last = true)]
85 nix_flags: Vec<String>,
86 },
87}
88
89struct WrapperConfig {
90 platform: cognos::Platform,
91 silent: bool,
92 verbose: u8,
93 format: rom_core::types::DisplayFormat,
94 legend_style: rom_core::types::LegendStyle,
95 summary_style: rom_core::types::SummaryStyle,
96 log_prefix_style: rom_core::types::LogPrefixStyle,
97 log_lines: Option<usize>,
98}
99
100pub fn run() -> eyre::Result<()> {
102 let cli = Cli::parse();
103
104 let default_filter = match cli.verbose {
106 0 => "rom=warn",
107 1 => "rom=info",
108 2 => "rom=debug",
109 _ => "rom=trace",
110 };
111 tracing_subscriber::fmt()
112 .with_env_filter(
113 EnvFilter::try_from_default_env()
114 .unwrap_or_else(|_| EnvFilter::new(default_filter)),
115 )
116 .with_target(false)
117 .with_writer(std::io::stderr)
118 .init();
119
120 let format = rom_core::types::DisplayFormat::from_str(&cli.format);
122 let legend_style = rom_core::types::LegendStyle::from_str(&cli.legend);
123 let summary_style = rom_core::types::SummaryStyle::from_str(&cli.summary);
124 let log_prefix_style =
125 rom_core::types::LogPrefixStyle::from_str(&cli.log_prefix);
126 let log_lines = cli.log_lines;
127 let silent = cli.silent;
128 let verbose = cli.verbose;
129 let json = cli.json;
130 let platform = cli
131 .platform
132 .as_deref()
133 .and_then(|platform| platform.parse().ok())
134 .unwrap_or_else(cognos::Platform::detect);
135
136 let program_name = std::env::args()
138 .next()
139 .and_then(|path| {
140 PathBuf::from(&path)
141 .file_name()
142 .and_then(|n| n.to_str())
143 .map(std::string::ToString::to_string)
144 })
145 .unwrap_or_else(|| "rom".to_string());
146
147 let make_config = |input_mode: rom_core::types::InputMode| {
148 rom_core::types::Config {
149 piping: false,
150 silent,
151 input_mode,
152 show_timers: true,
153 width: None,
154 format,
155 legend_style,
156 summary_style,
157 log_prefix_style,
158 log_line_limit: log_lines,
159 }
160 };
161
162 let cfg = WrapperConfig {
163 platform,
164 silent,
165 verbose,
166 format,
167 legend_style,
168 summary_style,
169 log_prefix_style,
170 log_lines,
171 };
172
173 match (&program_name[..], cli.command) {
174 ("rom-build", _) => {
176 let args: Vec<String> = std::env::args().skip(1).collect();
177 let (packages, nix_flags) = parse_args_with_separator(&args);
178 run_build_wrapper(packages, nix_flags, &cfg)?;
179 Ok(())
180 },
181
182 ("rom-shell", _) => {
184 let args: Vec<String> = std::env::args().skip(1).collect();
185 let (packages, nix_flags) = parse_args_with_separator(&args);
186 run_shell_wrapper(packages, nix_flags, &cfg)?;
187 Ok(())
188 },
189
190 (
192 _,
193 Some(Commands::Build {
194 packages,
195 nix_flags,
196 }),
197 ) => {
198 if packages.is_empty() && json {
199 let stdin = io::stdin();
200 let stdout = io::stdout();
201 return Ok(rom_core::monitor_stream(
202 make_config(rom_core::types::InputMode::Json),
203 stdin.lock(),
204 stdout.lock(),
205 )?);
206 }
207 if packages.is_empty() {
208 eyre::bail!(
209 "No package or flake specified for build\nUsage: rom build \
210 <package> [-- <flags>]\nExample: rom build nixpkgs#hello -- \
211 --rebuild"
212 );
213 }
214 run_build_wrapper(packages, nix_flags, &cfg)?;
215 Ok(())
216 },
217
218 (
220 _,
221 Some(Commands::Shell {
222 packages,
223 nix_flags,
224 }),
225 ) => {
226 if packages.is_empty() && json {
227 let stdin = io::stdin();
228 let stdout = io::stdout();
229 return Ok(rom_core::monitor_stream(
230 make_config(rom_core::types::InputMode::Json),
231 stdin.lock(),
232 stdout.lock(),
233 )?);
234 }
235 if packages.is_empty() {
236 eyre::bail!(
237 "No package or flake specified for shell\nUsage: rom shell \
238 <package> [-- <flags>]\nExample: rom shell nixpkgs#python3 -- \
239 --pure"
240 );
241 }
242 run_shell_wrapper(packages, nix_flags, &cfg)?;
243 Ok(())
244 },
245
246 (
248 _,
249 Some(Commands::Develop {
250 packages,
251 nix_flags,
252 }),
253 ) => {
254 if packages.is_empty() && json {
255 let stdin = io::stdin();
256 let stdout = io::stdout();
257 return Ok(rom_core::monitor_stream(
258 make_config(rom_core::types::InputMode::Json),
259 stdin.lock(),
260 stdout.lock(),
261 )?);
262 }
263 if packages.is_empty() {
264 eyre::bail!(
265 "No package or flake specified for develop\nUsage: rom develop \
266 <package> [-- <flags>]\nExample: rom develop nixpkgs#hello -- \
267 --impure"
268 );
269 }
270 run_develop_wrapper(packages, nix_flags, &cfg)?;
271 Ok(())
272 },
273
274 (_, None) => {
276 let input_mode = if json {
277 rom_core::types::InputMode::Json
278 } else {
279 rom_core::types::InputMode::Human
280 };
281 let stdin = io::stdin();
282 let stdout = io::stdout();
283 Ok(rom_core::monitor_stream(
284 make_config(input_mode),
285 stdin.lock(),
286 stdout.lock(),
287 )?)
288 },
289 }
290}
291
292#[must_use]
298pub fn parse_args_with_separator(
299 args: &[String],
300) -> (Vec<String>, Vec<String>) {
301 if let Some(pos) = args.iter().position(|arg| arg == "--") {
302 let before = args[..pos].to_vec();
304
305 let after = args[pos + 1..].to_vec();
307 (before, after)
308 } else {
309 (args.to_vec(), Vec::new())
312 }
313}
314
315fn nix_verbosity_flag(verbose: u8) -> String {
319 format!("-{}", "v".repeat(verbose.max(1) as usize))
320}
321
322fn run_build_wrapper(
323 packages: Vec<String>,
324 nix_flags: Vec<String>,
325 cfg: &WrapperConfig,
326) -> eyre::Result<()> {
327 if packages.is_empty() {
328 eyre::bail!(
329 "No package or flake specified for build\nUsage: rom build <package> \
330 [-- <flags>]\nExample: rom build nixpkgs#hello -- --rebuild"
331 );
332 }
333
334 let mut cmd_args = vec![
335 "build".to_string(),
336 nix_verbosity_flag(cfg.verbose),
337 "--log-format".to_string(),
338 "internal-json".to_string(),
339 ];
340 cmd_args.extend(packages);
341 cmd_args.extend(nix_flags);
342
343 let exit_code = run_monitored_command(cfg.platform.binary(), cmd_args, cfg)?;
344 if exit_code != 0 {
345 std::process::exit(exit_code);
346 }
347 Ok(())
348}
349
350fn run_shell_wrapper(
351 packages: Vec<String>,
352 nix_flags: Vec<String>,
353 cfg: &WrapperConfig,
354) -> eyre::Result<()> {
355 if packages.is_empty() {
356 eyre::bail!(
357 "No package or flake specified for shell\nUsage: rom shell <package> \
358 [-- <flags>]\nExample: rom shell nixpkgs#python3 -- --pure"
359 );
360 }
361
362 let mut monitor_args = vec![
364 "shell".to_string(),
365 nix_verbosity_flag(cfg.verbose),
366 "--log-format".to_string(),
367 "internal-json".to_string(),
368 ];
369 let shell_args: Vec<String> =
370 packages.iter().chain(nix_flags.iter()).cloned().collect();
371 monitor_args.extend(replace_command_with_exit(&shell_args));
372
373 let exit_code =
374 run_monitored_command(cfg.platform.binary(), monitor_args, cfg)?;
375
376 if exit_code != 0 {
377 std::process::exit(exit_code);
378 }
379
380 if !cfg.silent {
382 let mut shell_args = vec!["shell".to_string()];
383 shell_args.extend(packages);
384 shell_args.extend(nix_flags);
385
386 let status = Command::new(cfg.platform.binary())
387 .args(&shell_args)
388 .status()
389 .map_err(rom_core::error::RomError::Io)?;
390
391 std::process::exit(status.code().unwrap_or(1));
392 }
393
394 Ok(())
395}
396
397fn run_develop_wrapper(
398 packages: Vec<String>,
399 nix_flags: Vec<String>,
400 cfg: &WrapperConfig,
401) -> eyre::Result<()> {
402 let mut monitor_args = vec![
404 "develop".to_string(),
405 nix_verbosity_flag(cfg.verbose),
406 "--log-format".to_string(),
407 "internal-json".to_string(),
408 "--command".to_string(),
409 "true".to_string(),
410 ];
411 monitor_args.extend(packages.clone());
412 monitor_args.extend(nix_flags.clone());
413
414 let exit_code =
415 run_monitored_command(cfg.platform.binary(), monitor_args, cfg)?;
416
417 if exit_code != 0 {
418 std::process::exit(exit_code);
419 }
420
421 if !cfg.silent {
423 let mut develop_args = vec!["develop".to_string()];
424 develop_args.extend(packages);
425 develop_args.extend(nix_flags);
426
427 let status = Command::new(cfg.platform.binary())
428 .args(&develop_args)
429 .status()
430 .map_err(rom_core::error::RomError::Io)?;
431
432 std::process::exit(status.code().unwrap_or(1));
433 }
434
435 Ok(())
436}
437
438fn run_monitored_command(
439 command: &str,
440 args: Vec<String>,
441 cfg: &WrapperConfig,
442) -> eyre::Result<i32> {
443 let silent = cfg.silent;
444 let format = cfg.format;
445 let legend_style = cfg.legend_style;
446 let summary_style = cfg.summary_style;
447 let log_prefix_style = cfg.log_prefix_style;
448 let log_line_limit = cfg.log_lines;
449 use std::{
450 io::{BufRead, BufReader},
451 sync::{Arc, Mutex},
452 thread,
453 time::Duration,
454 };
455
456 let mut child = Command::new(command)
457 .args(&args)
458 .stdout(Stdio::piped())
459 .stderr(Stdio::piped())
460 .spawn()
461 .map_err(rom_core::error::RomError::Io)?;
462
463 let stderr = child.stderr.take().expect("Failed to capture stderr");
464 let stdout = child.stdout.take().expect("Failed to capture stdout");
465
466 let state = Arc::new(Mutex::new(rom_core::state::State::new()));
468 let state_clone = state.clone();
469 let render_state = state;
470
471 let processing_done = Arc::new(Mutex::new(false));
473 let processing_done_clone = processing_done.clone();
474
475 let start_time = Arc::new(Mutex::new(rom_core::state::current_time()));
477 let start_time_clone = start_time;
478
479 let log_buffer =
482 Arc::new(Mutex::new(std::collections::VecDeque::<String>::new()));
483 let log_buffer_clone = log_buffer.clone();
484 let log_buffer_render = log_buffer;
485
486 let stderr_thread = thread::spawn(move || {
488 use tracing::debug;
489 let reader = BufReader::new(stderr);
490 let mut json_count = 0;
491 let mut non_json_count = 0;
492
493 for line in reader.lines().map_while(Result::ok) {
494 if let Some(json_line) = line.strip_prefix("@nix ") {
496 json_count += 1;
497 if let Ok(action) = serde_json::from_str::<cognos::Actions>(json_line) {
498 debug!("Parsed JSON message #{}: {:?}", json_count, action);
499
500 let mut state = state_clone.lock().unwrap();
502 let derivation_count_before = state.derivation_infos.len();
503 rom_core::update::process_message(&mut state, action.clone());
504 rom_core::update::maintain_state(
505 &mut state,
506 rom_core::state::current_time(),
507 );
508 let derivation_count_after = state.derivation_infos.len();
509
510 match &action {
513 cognos::Actions::Message { msg, raw_msg, .. } => {
514 let display = raw_msg.as_deref().unwrap_or(msg.as_str());
516 let mut logs = log_buffer_clone.lock().unwrap();
517 logs.push_back(display.to_string());
518 if let Some(limit) = log_line_limit {
519 while logs.len() > limit {
520 logs.pop_front();
521 }
522 }
523 },
524 cognos::Actions::Result {
525 fields,
526 result_type,
527 id,
528 } => {
529 if matches!(result_type, cognos::ResultType::BuildLogLine)
531 && !fields.is_empty()
532 && let Some(log_text) = fields[0].as_str()
533 {
534 let use_color = !silent;
535 let prefix = state
536 .get_activity_prefix(*id, &log_prefix_style, use_color)
537 .unwrap_or_default();
538 let prefixed_log = format!("{prefix}{log_text}");
539 let mut logs = log_buffer_clone.lock().unwrap();
540 logs.push_back(prefixed_log);
541 if let Some(limit) = log_line_limit {
542 while logs.len() > limit {
543 logs.pop_front();
544 }
545 }
546 }
547 },
548 _ => {},
549 }
550
551 if derivation_count_after != derivation_count_before {
552 debug!(
553 "Derivation count changed: {} -> {}",
554 derivation_count_before, derivation_count_after
555 );
556 }
557 } else {
558 debug!("Failed to parse JSON: {}", json_line);
559 }
560 } else {
561 non_json_count += 1;
563 let mut logs = log_buffer_clone.lock().unwrap();
564 logs.push_back(line.clone());
565 if let Some(limit) = log_line_limit {
567 while logs.len() > limit {
568 logs.pop_front();
569 }
570 }
571 }
572 }
573 debug!(
574 "Stderr thread finished: {} JSON messages, {} non-JSON lines",
575 json_count, non_json_count
576 );
577 *processing_done_clone.lock().unwrap() = true;
578 });
579
580 let stdout_lines = Arc::new(Mutex::new(Vec::new()));
582 let stdout_lines_clone = stdout_lines.clone();
583
584 let stdout_thread = thread::spawn(move || {
585 let reader = BufReader::new(stdout);
586 for line in reader.lines().map_while(Result::ok) {
587 stdout_lines_clone.lock().unwrap().push(line);
588 }
589 });
590
591 let render_thread = thread::spawn(move || {
593 use rom_core::display::{Display, DisplayConfig};
594
595 let display_config = DisplayConfig {
596 show_timers: true,
597 max_tree_depth: 10,
598 max_visible_lines: 100,
599 use_color: true,
600 format,
601 legend_style,
602 summary_style,
603 icons: rom_core::icons::detect(),
604 };
605
606 let mut display = Display::new(io::stderr(), display_config).unwrap();
607 let mut last_timer_display: Option<String> = None;
608
609 loop {
611 thread::sleep(Duration::from_millis(100));
612 let done = *processing_done.lock().unwrap();
613
614 let state = render_state.lock().unwrap();
615 let has_activity = !state.derivation_infos.is_empty()
616 || !state.full_summary.running_builds.is_empty()
617 || !state.full_summary.planned_builds.is_empty();
618
619 {
620 let logs: Vec<String> = if silent {
623 vec![]
624 } else {
625 log_buffer_render.lock().unwrap().iter().cloned().collect()
626 };
627
628 if has_activity || state.progress_state != ProgressState::JustStarted {
629 if last_timer_display.is_some() {
631 last_timer_display = None;
632 }
633 let _ = display.render(&state, &logs);
634 } else {
635 let start = *start_time_clone.lock().unwrap();
637 let elapsed = rom_core::state::current_time() - start;
638 let timer_text =
639 format!("⏱ {}", rom_core::display::format_duration(elapsed));
640
641 if last_timer_display.as_ref() != Some(&timer_text) {
643 let _ = display.render(&state, &logs);
644 last_timer_display = Some(timer_text);
645 }
646 }
647 }
648
649 if done {
650 break;
651 }
652 }
653
654 thread::sleep(Duration::from_millis(50));
656 {
657 let mut state = render_state.lock().unwrap();
658 rom_core::update::finish_state(&mut state);
659 let _ = display.render_final(&state);
660 }
661 });
662
663 let status = child.wait().map_err(rom_core::error::RomError::Io)?;
665
666 let _ = stderr_thread.join();
668 let _ = stdout_thread.join();
669 let _ = render_thread.join();
670
671 let stdout_lines = stdout_lines.lock().unwrap();
673 for line in stdout_lines.iter() {
674 use std::io::Write;
675 let _ = writeln!(std::io::stdout(), "{line}");
676 }
677
678 Ok(status.code().unwrap_or(1))
679}
680
681pub fn replace_command_with_exit(args: &[String]) -> Vec<String> {
683 let mut result = Vec::new();
684 let mut skip_next = false;
685
686 for arg in args {
687 if skip_next {
688 skip_next = false;
689 continue;
690 }
691
692 if arg == "--command" || arg == "-c" {
693 skip_next = true;
695 continue;
696 }
697
698 result.push(arg.clone());
699 }
700
701 result.push("--command".to_string());
703 result.push("sh".to_string());
704 result.push("-c".to_string());
705 result.push("exit".to_string());
706
707 result
708}