diff --git a/Cargo.lock b/Cargo.lock index ed750fd8..471f801f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,12 +429,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -772,12 +766,6 @@ dependencies = [ "syn", ] -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "dryoc" version = "0.6.2" @@ -1585,18 +1573,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1818,27 +1794,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg", -] - [[package]] name = "potential_utf" version = "0.1.2" @@ -1888,7 +1843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -1928,7 +1883,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", "socket2", @@ -2107,13 +2062,13 @@ dependencies = [ "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", - "portable-pty", "ratatui", "redox-pkg", "redox_installer", "redoxer", "regex", "serde", + "strip-ansi-escapes", "tempfile", "termion", "toml 0.8.23", @@ -2535,17 +2490,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serial2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" -dependencies = [ - "cfg-if 1.0.1", - "libc", - "winapi", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2557,22 +2501,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shlex" version = "1.3.0" @@ -3579,15 +3507,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 05494951..9d25c8ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,15 @@ name = "cookbook" path = "src/lib.rs" doctest = false +[features] +#TODO: Actually make without tui feature works +default = ["tui"] +tui = ["ratatui", "ansi-to-tui", "filedescriptor", "strip-ansi-escapes"] + [dependencies] anyhow = "1" -blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar +# blake3 1.5.4 is incompatible with 0.3 dependency from pkgar +blake3 = "=1.5.3" libc = "0.2" ignore = "0.4" object = { version = "0.36", features = ["build_core"] } @@ -30,22 +36,23 @@ pbr = "1.0.2" pkgar = { path = "pkgar/pkgar" } pkgar-core = { path = "pkgar/pkgar-core" } pkgar-keys = { path = "pkgar/pkgar-keys" } -portable-pty = "0.9.0" redox-pkg = "0.2.8" +redox_installer = "0.2.37" redoxer = "0.2.56" regex = "1.11" serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" -filedescriptor = "0.8.3" -ansi-to-tui = "7.0.0" -redox_installer = "0.2.37" +filedescriptor = { version = "0.8.3", optional = true } +ansi-to-tui = { version = "7.0.0", optional = true } +strip-ansi-escapes = { version = "0.2.1", optional = true } [dependencies.ratatui] version = "0.29.0" default-features = false features = ["termion"] +optional = true [dev-dependencies] tempfile = "3" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 69eecb2a..2d08507c 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -5,8 +5,10 @@ use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; use cookbook::cook::fs::{create_target_dir, run_command}; use cookbook::cook::package::package; -use cookbook::cook::pty::{PtyOut, UnixSlavePty, setup_pty}; +use cookbook::cook::pty::{PtyOut, UnixSlavePty, flush_pty, setup_pty}; +use cookbook::cook::script::KILL_ALL_PID; use cookbook::cook::tree::{display_tree_entry, format_size}; +use cookbook::log_to_pty; use cookbook::recipe::{BuildKind, CookRecipe}; use pkg::PackageName; use pkg::package::PackageError; @@ -72,6 +74,7 @@ struct CliConfig { cookbook_dir: PathBuf, repo_dir: PathBuf, sysroot_dir: PathBuf, + logs_dir: Option, category: Option, filesystem: Option, with_package_deps: bool, @@ -140,6 +143,12 @@ impl CliConfig { //FIXME: This config is unused as redox-pkg harcoded this to $PWD/recipes cookbook_dir: current_dir.join("recipes"), repo_dir: current_dir.join("repo"), + // build dir here is hardcoded in repo_builder as well + logs_dir: if get_config().cook.tui_logs { + Some(current_dir.join("build/logs")) + } else { + None + }, category: None, sysroot_dir: if cfg!(target_os = "redox") { PathBuf::from("/") @@ -342,6 +351,10 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec), + FlushLog(PackageName, PathBuf), FetchThreadFinished, CookThreadFinished, } @@ -676,6 +690,55 @@ impl TuiApp { } } + pub fn get_active_name(&self) -> Option { + if self.log_view_job == JobType::Cook { + self.active_cook.clone() + } else { + self.active_fetch.clone() + } + } + + pub fn get_active_log( + &self, + ) -> ( + Option, + Option<&Vec>, + Option>, + ) { + let active_name = self.get_active_name(); + let (log_text, log_line) = if let Some(active_name) = active_name.as_ref() { + self.get_recipe_log(active_name) + } else { + (None, None) + }; + + (active_name, log_text, log_line) + } + + pub fn get_recipe_log( + &self, + recipe_name: &PackageName, + ) -> (Option<&Vec>, Option>) { + let log_text = self.logs.get(recipe_name); + let log_line = if let Some(b) = self.log_byte_buffer.get(recipe_name) { + Some(String::from_utf8_lossy(b)) + } else { + None + }; + (log_text, log_line) + } + + pub fn write_log(&self, recipe_name: &PackageName, log_path: &PathBuf) -> anyhow::Result<()> { + let (Some(logs), line) = self.get_recipe_log(recipe_name) else { + return Ok(()); + }; + let str = strip_ansi_escapes::strip_str(join_logs(logs, line)); + if !str.trim_end().is_empty() { + fs::write(log_path, str)?; + } + return Ok(()); + } + // Update the state based on a message from a worker thread fn update_status(&mut self, update: StatusUpdate) { let (name, new_status) = match update { @@ -711,6 +774,12 @@ impl TuiApp { } return; } + StatusUpdate::FlushLog(name, path) => { + // TODO: This blocks the TUI, maybe open separate thread? + // FIXME: handle error here? + let _ = self.write_log(&name, &path); + return; + } StatusUpdate::Cooked(recipe) => { if self.active_cook.as_ref() == Some(&recipe.name) { self.active_cook = None; @@ -782,15 +851,26 @@ fn run_tui_cook( .send(StatusUpdate::StartCook(name.clone())) .unwrap(); let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); - let logger = Some((&mut stdout_writer, &mut stderr_writer)); + let mut logger = Some((&mut stdout_writer, &mut stderr_writer)); 'again: loop { - match handle_cook( + let handler = handle_cook( &recipe, &cooker_config, source_dir.clone(), is_deps, &logger, - ) { + ); + if let Some(log_path) = cooker_config.logs_dir.as_ref() { + if let Err(err_ctx) = &handler { + log_to_pty!(&logger, "\n{:?}", err_ctx) + } + flush_pty(&mut logger); + let log_path = log_path.join(format!("{}.log", name.as_str())); + cooker_status_tx + .send(StatusUpdate::FlushLog(name.clone(), log_path)) + .unwrap_or_default(); + } + match handler { Ok(()) => { cooker_status_tx .send(StatusUpdate::Cooked(recipe)) @@ -867,10 +947,24 @@ fn run_tui_cook( .unwrap(); let (mut stdout_writer, mut stderr_writer) = setup_logger(&fetcher_status_tx, &name); - let logger = Some((&mut stdout_writer, &mut stderr_writer)); + let mut logger = Some((&mut stdout_writer, &mut stderr_writer)); 'again: loop { - match handle_fetch(&recipe, &fetcher_config, &logger) { + let handler = handle_fetch(&recipe, &fetcher_config, &logger); + if let Some(log_path) = fetcher_config.logs_dir.as_ref() + // successful fetch log usually not that helpful + && handler.is_err() + { + if let Err(err_ctx) = &handler { + log_to_pty!(&logger, "\n{:?}", err_ctx) + } + flush_pty(&mut logger); + let log_path = log_path.join(format!("{}.log", name.as_str())); + fetcher_status_tx + .send(StatusUpdate::FlushLog(name.clone(), log_path)) + .unwrap_or_default(); + } + match handler { Ok(source_dir) => { fetcher_status_tx .send(StatusUpdate::Fetched(recipe.clone())) @@ -1041,7 +1135,7 @@ fn run_tui_cook( ); f.render_stateful_widget(cook_list, cook_chunk, &mut app.cook_list_state); - let (active_name, log_text, log_line) = get_active_log(&app); + let (active_name, log_text, log_line) = app.get_active_log(); let log_title = if let Some(active_name) = active_name { format!( " {} Log: {} ", @@ -1152,16 +1246,11 @@ fn run_tui_cook( if let Some((app, res)) = handle_prompt_input(&event, &mut app) { prompting.swap(res as u32, Ordering::SeqCst); if res == PromptOption::Exit { - let (name, log, line) = get_active_log(&app); + let (name, log, line) = app.get_active_log(); if let Some(name) = name && let Some(log) = log { - let mut logs = log.join("\n"); - if let Some(line) = line { - logs.push_str("\n"); - logs.push_str(handle_cr(&line)); - } - app.dump_logs_on_exit = Some((name.to_owned(), logs)); + app.dump_logs_on_exit = Some((name.to_owned(), join_logs(log, line))); } running.store(false, Ordering::SeqCst); } @@ -1194,38 +1283,20 @@ fn run_tui_cook( Ok(app.dump_logs_on_exit) } +fn join_logs(log: &Vec, line: Option>) -> String { + let mut logs = log.join("\n"); + if let Some(line) = line { + logs.push_str("\n"); + logs.push_str(handle_cr(&line)); + } + logs +} + fn handle_cr<'a>(buffer: &'a Cow<'_, str>) -> &'a str { let st = buffer.trim_end(); st.rsplit('\r').next().unwrap_or(&st) } -fn get_active_log( - app: &TuiApp, -) -> ( - Option, - Option<&Vec>, - Option>, -) { - let active_name = if app.log_view_job == JobType::Cook { - app.active_cook.clone() - } else { - app.active_fetch.clone() - }; - let log_text = if let Some(active_name) = &active_name { - app.logs.get(active_name) - } else { - None - }; - let log_line = if let Some(active_name) = &active_name - && let Some(b) = app.log_byte_buffer.get(active_name) - { - Some(String::from_utf8_lossy(b)) - } else { - None - }; - (active_name, log_text, log_line) -} - fn handle_main_event(app: &mut TuiApp, event: &Event) { match event { Event::Key(key) => match key { @@ -1238,12 +1309,11 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) { Key::Char('c') => { // as compilers still running, we use this way to stop it let pid = std::process::id(); - Command::new("pkill") - .arg("-9") - .arg("-P") - .arg(pid.to_string()) + Command::new("bash") + .arg("-c") + .arg(KILL_ALL_PID.replace("$PID", &pid.to_string())) .spawn() - .expect("unable to spawn pkill"); + .expect("unable to spawn kill"); } Key::Up => { app.auto_scroll = false; diff --git a/src/config.rs b/src/config.rs index 460fa43d..f014092e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,9 @@ pub struct CookConfigOpt { /// whether to use TUI to allow parallel build /// default value is yes if "CI" env unset and STDIN is open. pub tui: Option, + /// whether to write logs to build/logs dir + /// only usable when tui is used + pub tui_logs: Option, /// whether to ignore build errors pub nonstop: Option, /// whether to print success recipes info and warnings @@ -24,6 +27,7 @@ pub struct CookConfig { pub offline: bool, pub jobs: usize, pub tui: bool, + pub tui_logs: bool, pub nonstop: bool, pub verbose: bool, } @@ -34,6 +38,7 @@ impl From for CookConfig { offline: value.offline.unwrap(), jobs: value.jobs.unwrap(), tui: value.tui.unwrap(), + tui_logs: value.tui_logs.unwrap(), nonstop: value.nonstop.unwrap(), verbose: value.verbose.unwrap(), } @@ -75,6 +80,9 @@ pub fn init_config() { .unwrap_or(1), )); } + if config.cook_opt.tui_logs.is_none() { + config.cook_opt.tui_logs = config.cook_opt.tui; + } if config.cook_opt.offline.is_none() { config.cook_opt.offline = Some(extract_env("COOKBOOK_OFFLINE", false)); } diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index 4268f08d..75f69f87 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -19,25 +19,10 @@ use std::{ time::SystemTime, }; -use crate::is_redox; +use crate::{is_redox, log_to_pty}; use crate::REMOTE_PKG_SOURCE; -macro_rules! log_warn { - ($logger:expr, $($arg:tt)+) => { - use std::io::Write; - - if $logger.is_some() { - let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( - format!($($arg)+) - .as_bytes(), - ); - } else { - eprintln!($($arg)+); - } - }; -} - fn auto_deps_from_dynamic_linking( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, @@ -61,7 +46,7 @@ fn auto_deps_from_dynamic_linking( }; if visited.contains(&dir) { #[cfg(debug_assertions)] - log_warn!( + log_to_pty!( logger, "DEBUG: auto_deps => Skipping `{dir:?}` (already visited)" ); @@ -111,7 +96,7 @@ fn auto_deps_from_dynamic_linking( continue; }; if let Ok(relative_path) = path.strip_prefix(stage_dir) { - log_warn!(logger, "DEBUG: {} needs {}", relative_path.display(), name); + log_to_pty!(logger, "DEBUG: {} needs {}", relative_path.display(), name); } needed.insert(name.to_string()); } @@ -145,7 +130,7 @@ fn auto_deps_from_dynamic_linking( continue; }; if needed.contains(child_name) { - log_warn!(logger, "DEBUG: {} provides {}", dep, child_name); + log_to_pty!(logger, "DEBUG: {} provides {}", dep, child_name); deps.insert(dep.clone()); missing.remove(child_name); } @@ -155,7 +140,7 @@ fn auto_deps_from_dynamic_linking( } for name in missing { - log_warn!(logger, "WARN: {} missing", name); + log_to_pty!(logger, "WARN: {} missing", name); } deps @@ -225,7 +210,7 @@ pub fn build( if sysroot_dir.is_dir() { let sysroot_modified = modified_dir(&sysroot_dir)?; if sysroot_modified < source_modified || sysroot_modified < deps_modified { - log_warn!( + log_to_pty!( logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), @@ -275,7 +260,7 @@ pub fn build( if stage_dir.is_dir() { let stage_modified = modified_dir(&stage_dir)?; if stage_modified < source_modified || stage_modified < deps_modified { - log_warn!( + log_to_pty!( logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 7f569fb5..b15a3554 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -3,6 +3,7 @@ use crate::cook::fs::*; use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::is_redox; +use crate::log_to_pty; use crate::recipe::BuildKind; use crate::recipe::Recipe; use crate::{blake3, recipe::SourceRecipe}; @@ -10,21 +11,6 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -macro_rules! log_warn { - ($logger:expr, $($arg:tt)+) => { - use std::io::Write; - - if $logger.is_some() { - let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( - format!($($arg)+) - .as_bytes(), - ); - } else { - eprintln!($($arg)+); - } - }; -} - pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result { if show_progress { blake3::blake3_progress(&path) @@ -120,7 +106,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)? { - log_warn!( + log_to_pty!( logger, "[DEBUG]: {} is newer than {}", path, @@ -266,7 +252,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result Result Result Result { if !source_dir.is_dir() { - log_warn!( + log_to_pty!( logger, "WARNING: Recipe without source section expected source dir at '{}'", source_dir.display(), diff --git a/src/cook/pty.rs b/src/cook/pty.rs index 96bae8c8..ae1c2aec 100644 --- a/src/cook/pty.rs +++ b/src/cook/pty.rs @@ -1,11 +1,12 @@ use anyhow::{Error, bail}; use filedescriptor::FileDescriptor; use libc::{self, winsize}; -use std::io::Read; +use std::io::{Read, Write}; use std::os::fd::FromRawFd; use std::os::unix::io::AsRawFd; use std::os::unix::process::CommandExt; use std::process::Child; +use std::time::Duration; use std::{io, mem, ptr}; use std::{ io::{PipeReader, PipeWriter}, @@ -17,10 +18,9 @@ pub use std::os::unix::io::RawFd; #[macro_export] macro_rules! log_to_pty { ($logger:expr, $($arg:tt)+) => { - use std::io::Write; - if $logger.is_some() { - let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + use std::io::Write; + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( format!($($arg)+) .as_bytes(), ); @@ -57,6 +57,17 @@ pub fn setup_pty() -> ( (pty_reader, log_reader, pipes) } +pub fn flush_pty(logger: &mut PtyOut) { + let Some((pty, file)) = logger else { + return; + }; + // Not sure if flush actually working + let _ = pty.flush(); + std::thread::sleep(Duration::from_millis(100)); + let _ = file.flush(); + std::thread::sleep(Duration::from_millis(100)); +} + pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result { match stdout_pipe { Some(stdout) => stdout.0.spawn_command(command.into()), @@ -282,6 +293,10 @@ impl PtyFd { Ok(child) } + + fn flush(&mut self) -> std::io::Result<()> { + self.0.flush() + } } /// Represents the master end of a pty. @@ -319,6 +334,9 @@ impl UnixSlavePty { fn spawn_command(&self, builder: &mut Command) -> Result { Ok(self.fd.spawn_command(builder)?) } + fn flush(&mut self) -> Result<(), anyhow::Error> { + Ok(self.fd.flush()?) + } } impl UnixMasterPty { diff --git a/src/cook/script.rs b/src/cook/script.rs index f7cb158b..a9e97d04 100644 --- a/src/cook/script.rs +++ b/src/cook/script.rs @@ -351,3 +351,19 @@ if [ "$(git rev-parse HEAD)" != "$(git rev-parse $ORIGIN_BRANCH)" ] then git checkout -B "$(echo "$ORIGIN_BRANCH" | cut -d / -f 2-)" "$ORIGIN_BRANCH" fi"#; + +pub static KILL_ALL_PID: &str = r#" +THISPID=$$ +CHILDREN=$(ps -o pid= --ppid $PID | grep -v $THISPID); + +ALL_DESCENDANTS=''; + +while [ -n "$CHILDREN" ]; do + ALL_DESCENDANTS="$ALL_DESCENDANTS $CHILDREN"; + CHILDREN=$(ps -o pid= --ppid $(echo $CHILDREN) | tr '\n' ' '); +done; + +if [ -n "$ALL_DESCENDANTS" ]; then + kill -9 $ALL_DESCENDANTS; +fi +"#;