From 605bfcfbd4c57dbabe2f1750adbe12337ecd10ad Mon Sep 17 00:00:00 2001 From: Wildan Mubarok Date: Mon, 1 Sep 2025 15:39:13 +0000 Subject: [PATCH 01/26] Add initial repo bin --- src/bin/repo.rs | 242 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 src/bin/repo.rs diff --git a/src/bin/repo.rs b/src/bin/repo.rs new file mode 100644 index 00000000..79a188fd --- /dev/null +++ b/src/bin/repo.rs @@ -0,0 +1,242 @@ +use std::error::Error; +use std::fmt::format; +use std::path::{Path, PathBuf}; +use std::process; +use std::{env, fs}; + +// A repo manager, to replace repo.sh + +const REPO_HELP_STR: &str = r#" + Usage: repo [flags] ... + + command list: + fetch download recipe sources + cook build recipe packages + unfetch delete recipe sources + clean delete recipe artifacts + push extract package into sysroot + + common flags: + --cookbook= the "recipes" folder, default to $PWD/recipes + --repo= the "repo" folder, default to $PWD/repo + --sysroot= the "root" folder used for "push" command + For Redox, defaults to "/", else default to $PWD/sysroot + + cook flags: + --with-package-deps include package deps + --offline prefer to not use network + --nonstop keep running even a recipe build failed + --all apply to all recipes in + -q, --quiet surpress build logs unless error +"#; + +struct Config { + cookbook_dir: PathBuf, + repo_dir: PathBuf, + sysroot_dir: PathBuf, + with_package_deps: bool, + offline: bool, + nonstop: bool, + all: bool, + quiet: bool, +} + +impl Config { + fn new() -> Result { + let current_dir = env::current_dir()?; + Ok(Config { + cookbook_dir: current_dir.join("recipes"), + repo_dir: current_dir.join("repo"), + sysroot_dir: if cfg!(target_os = "redox") { + PathBuf::from("/") + } else { + current_dir.join("sysroot") + }, + with_package_deps: false, + offline: false, + nonstop: false, + all: false, + quiet: false, + }) + } +} + +fn main() -> Result<(), Box> { + let args: Vec = env::args().skip(1).collect(); + + if args.is_empty() || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) { + println!("{}", REPO_HELP_STR); + return Ok(()); + } + + let mut config = Config::new()?; + let mut command: Option = None; + let mut recipe_paths: Vec = Vec::new(); + + for arg in args { + if arg.starts_with("--") { + if let Some((key, value)) = arg.split_once('=') { + match key { + "--cookbook" => config.cookbook_dir = PathBuf::from(value), + "--repo" => config.repo_dir = PathBuf::from(value), + "--sysroot" => config.sysroot_dir = Some(PathBuf::from(value)), + _ => { + eprintln!("Error: Unknown flag with value: {}", arg); + process::exit(1); + } + } + } else { + match arg.as_str() { + "--with-package-deps" => config.with_package_deps = true, + "--offline" => config.offline = true, + "--nonstop" => config.nonstop = true, + "--all" => config.all = true, + "--quiet" => config.quiet = true, + _ => { + eprintln!("Error: Unknown flag: {}", arg); + process::exit(1); + } + } + } + } else if arg.starts_with('-') { + match arg.as_str() { + "-q" => config.quiet = true, + _ => { + eprintln!("Error: Unknown flag: {}", arg); + process::exit(1); + } + } + } else if command.is_none() { + // The first non-flag argument is the command + command = Some(arg); + } else { + // Subsequent non-flag arguments are recipe names + if Some(path) = pkg::recipes::find(arg) { + recipe_paths.push(path); + } else { + return Err(format!("Error: recipe not found '{arg}'")); + } + } + } + + let command = command.ok_or("Error: No command specified.")?; + + if !config.all && recipe_paths.is_empty() { + return Err("Error: No recipe names provided and --all flag was not used.".into()); + } + if config.all && !recipe_paths.is_empty() { + return Err("Error: Cannot specify recipe names when using the --all flag.".into()); + } + + if config.all { + recipe_paths = pkg::recipes::list(""); + } + + for recipe_path in &recipe_paths { + match command.as_str() { + "fetch" => handle_fetch(recipe_path, &config)?, + "cook" => handle_cook(recipe_path, &config)?, + "unfetch" => handle_unfetch(recipe_path, &config)?, + "clean" => handle_clean(recipe_path, &config)?, + "push" => handle_push(recipe_path, &config)?, + _ => { + eprintln!("Error: Unknown command '{}'\n", command); + println!("{}", REPO_HELP_STR); + process::exit(1); + } + } + } + + println!( + "\nCommand '{}' completed for all specified recipes.", + command + ); + Ok(()) +} + +fn handle_fetch(recipe_path: &Path, config: &Config) -> Result<(), String> { + let mut cmd = Command::new("cook"); + cmd.arg("--fetch-only"); + if config.with_package_deps { + cmd.arg("--with-package-deps"); + } + if config.offline { + cmd.arg("--offline"); + } + if config.quiet { + cmd.arg("--quiet"); + } + cmd.arg(recipe_path); + let status = cmd + .status() + .map_err(|e| format!("Failed to execute cook command: {}", e))?; + if !status.success() && !config.nonstop { + return Err(format!( + "Cook command failed for recipe '{}' with exit code: {}", + recipe_name, + status.code().unwrap_or(1) + )); + } + Ok(()) +} + +fn handle_cook(recipe_path: &Path, config: &Config) -> Result<(), String> { + let mut cmd = Command::new("cook"); + cmd.arg(recipe_path); + if config.with_package_deps { + cmd.arg("--with-package-deps"); + } + if config.offline { + cmd.arg("--offline"); + } + if config.quiet { + cmd.arg("--quiet"); + } + let status = cmd + .status() + .map_err(|e| format!("Failed to execute cook command: {}", e))?; + if !status.success() && !config.nonstop { + return Err(format!( + "Cook command failed for recipe '{}' with exit code: {}", + recipe_name, + status.code().unwrap_or(1) + )); + } + Ok(()) +} + +fn handle_unfetch(recipe_path: &Path, config: &Config) -> Result<(), String> { + let dir = recipe_path.join("source"); + if dir.exists() { + fs::remove_dir_all(dir) + .map_err(|err| format!("failed to delete '{}': {:?}", recipe_path, errF))?; + } + Ok(()) +} + +fn handle_clean(recipe_path: &Path, config: &Config) -> Result<(), String> { + let dir = recipe_path.join("target"); + if dir.exists() { + fs::remove_dir_all(dir) + .map_err(|err| format!("failed to delete '{}': {:?}", recipe_path, errF))?; + } + Ok(()) +} + +fn handle_push(recipe_path: &Path, config: &Config) -> Result<(), String> { + let public_path = "build/id_ed25519.pub.toml"; + pkgar::extract( + public_path, + config.sysroot_dir.as_path(), + sysroot_dir_tmp.to_str().unwrap(), + ) + .map_err(|err| { + format!( + "failed to install '{}' in '{}': {:?}", + archive_path.display(), + config.sysroot_dir.display(), + err + ) + })?; + Ok(()) +} From 79f1eca998b41820c0407ebb586058abee3e7d83 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Thu, 23 Oct 2025 12:22:43 +0700 Subject: [PATCH 02/26] Fix err, use anyhow --- Cargo.lock | 5 +-- Cargo.toml | 1 + src/bin/repo.rs | 87 +++++++++++++++++++++++-------------------------- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce7dbe51..4d6b149a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,9 +128,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arg_parser" @@ -1863,6 +1863,7 @@ dependencies = [ name = "redox_cookbook" version = "0.1.0" dependencies = [ + "anyhow", "blake3 1.5.3", "ignore", "object", diff --git a/Cargo.toml b/Cargo.toml index e163cf5e..d82ada6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ path = "src/lib.rs" doctest = false [dependencies] +anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar ignore = "0.4" object = { version = "0.36", features = ["build_core"] } diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 79a188fd..ec0a604f 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,9 +1,10 @@ -use std::error::Error; -use std::fmt::format; +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; -use std::process; +use std::process::{self, Command}; use std::{env, fs}; +use anyhow::{Context, anyhow}; + // A repo manager, to replace repo.sh const REPO_HELP_STR: &str = r#" @@ -30,7 +31,7 @@ const REPO_HELP_STR: &str = r#" -q, --quiet surpress build logs unless error "#; -struct Config { +struct CliConfig { cookbook_dir: PathBuf, repo_dir: PathBuf, sysroot_dir: PathBuf, @@ -41,10 +42,10 @@ struct Config { quiet: bool, } -impl Config { +impl CliConfig { fn new() -> Result { let current_dir = env::current_dir()?; - Ok(Config { + Ok(CliConfig { cookbook_dir: current_dir.join("recipes"), repo_dir: current_dir.join("repo"), sysroot_dir: if cfg!(target_os = "redox") { @@ -61,17 +62,21 @@ impl Config { } } -fn main() -> Result<(), Box> { +fn main() { + main_inner().unwrap(); +} + +fn main_inner() -> anyhow::Result<()> { let args: Vec = env::args().skip(1).collect(); if args.is_empty() || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) { println!("{}", REPO_HELP_STR); - return Ok(()); + process::exit(1); } - let mut config = Config::new()?; + let mut config = CliConfig::new()?; let mut command: Option = None; - let mut recipe_paths: Vec = Vec::new(); + let mut recipe_paths: BTreeSet = BTreeSet::new(); for arg in args { if arg.starts_with("--") { @@ -79,7 +84,7 @@ fn main() -> Result<(), Box> { match key { "--cookbook" => config.cookbook_dir = PathBuf::from(value), "--repo" => config.repo_dir = PathBuf::from(value), - "--sysroot" => config.sysroot_dir = Some(PathBuf::from(value)), + "--sysroot" => config.sysroot_dir = PathBuf::from(value), _ => { eprintln!("Error: Unknown flag with value: {}", arg); process::exit(1); @@ -111,21 +116,21 @@ fn main() -> Result<(), Box> { command = Some(arg); } else { // Subsequent non-flag arguments are recipe names - if Some(path) = pkg::recipes::find(arg) { - recipe_paths.push(path); + if let Some(path) = pkg::recipes::find(&arg) { + recipe_paths.insert(path.to_owned()); } else { - return Err(format!("Error: recipe not found '{arg}'")); + panic!("Error: recipe not found '{arg}'"); } } } - let command = command.ok_or("Error: No command specified.")?; + let command = command.ok_or("Error: No command specified.").unwrap(); if !config.all && recipe_paths.is_empty() { - return Err("Error: No recipe names provided and --all flag was not used.".into()); + panic!("Error: No recipe names provided and --all flag was not used."); } if config.all && !recipe_paths.is_empty() { - return Err("Error: Cannot specify recipe names when using the --all flag.".into()); + panic!("Error: Cannot specify recipe names when using the --all flag."); } if config.all { @@ -154,7 +159,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn handle_fetch(recipe_path: &Path, config: &Config) -> Result<(), String> { +fn handle_fetch(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { let mut cmd = Command::new("cook"); cmd.arg("--fetch-only"); if config.with_package_deps { @@ -167,20 +172,18 @@ fn handle_fetch(recipe_path: &Path, config: &Config) -> Result<(), String> { cmd.arg("--quiet"); } cmd.arg(recipe_path); - let status = cmd - .status() - .map_err(|e| format!("Failed to execute cook command: {}", e))?; + let status = cmd.status().context("Failed to execute cook command")?; if !status.success() && !config.nonstop { - return Err(format!( + return Err(anyhow!( "Cook command failed for recipe '{}' with exit code: {}", - recipe_name, + recipe_path.display(), status.code().unwrap_or(1) )); } Ok(()) } -fn handle_cook(recipe_path: &Path, config: &Config) -> Result<(), String> { +fn handle_cook(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { let mut cmd = Command::new("cook"); cmd.arg(recipe_path); if config.with_package_deps { @@ -192,51 +195,43 @@ fn handle_cook(recipe_path: &Path, config: &Config) -> Result<(), String> { if config.quiet { cmd.arg("--quiet"); } - let status = cmd - .status() - .map_err(|e| format!("Failed to execute cook command: {}", e))?; + let status = cmd.status().context("Failed to execute cook command")?; if !status.success() && !config.nonstop { - return Err(format!( + return Err(anyhow!( "Cook command failed for recipe '{}' with exit code: {}", - recipe_name, + recipe_path.display(), status.code().unwrap_or(1) )); } Ok(()) } -fn handle_unfetch(recipe_path: &Path, config: &Config) -> Result<(), String> { +fn handle_unfetch(recipe_path: &Path, _config: &CliConfig) -> anyhow::Result<()> { let dir = recipe_path.join("source"); if dir.exists() { - fs::remove_dir_all(dir) - .map_err(|err| format!("failed to delete '{}': {:?}", recipe_path, errF))?; + fs::remove_dir_all(dir).context(format!("failed to delete {}", recipe_path.display()))?; } Ok(()) } -fn handle_clean(recipe_path: &Path, config: &Config) -> Result<(), String> { +fn handle_clean(recipe_path: &Path, _config: &CliConfig) -> anyhow::Result<()> { let dir = recipe_path.join("target"); if dir.exists() { - fs::remove_dir_all(dir) - .map_err(|err| format!("failed to delete '{}': {:?}", recipe_path, errF))?; + fs::remove_dir_all(dir).context(format!("failed to delete {}", recipe_path.display()))?; } Ok(()) } -fn handle_push(recipe_path: &Path, config: &Config) -> Result<(), String> { +fn handle_push(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { let public_path = "build/id_ed25519.pub.toml"; pkgar::extract( public_path, config.sysroot_dir.as_path(), - sysroot_dir_tmp.to_str().unwrap(), + config.sysroot_dir.to_str().unwrap(), ) - .map_err(|err| { - format!( - "failed to install '{}' in '{}': {:?}", - archive_path.display(), - config.sysroot_dir.display(), - err - ) - })?; - Ok(()) + .context(format!( + "failed to install '{}' in '{}'", + recipe_path.display(), + config.sysroot_dir.display(), + )) } From 8e2ac316e493a2b4254a07a6ca45f303bee84e65 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 16:18:11 +0700 Subject: [PATCH 03/26] Compiling with direct call to cook --- src/bin/cook.rs | 32 +---- src/bin/repo.rs | 275 ++++++++++++++++++++++++---------------- src/bin/repo_builder.rs | 3 + src/config.rs | 84 +++++++++++- src/cook/cook_build.rs | 4 + src/cook/fetch.rs | 33 +++-- src/cook/package.rs | 19 +-- src/recipe.rs | 67 ++++++---- 8 files changed, 335 insertions(+), 182 deletions(-) diff --git a/src/bin/cook.rs b/src/bin/cook.rs index 38d126c9..d5299d70 100644 --- a/src/bin/cook.rs +++ b/src/bin/cook.rs @@ -1,36 +1,17 @@ -use std::collections::BTreeSet; use std::path::Path; use std::{env, process}; use cookbook::WALK_DEPTH; use cookbook::cook::fetch::{fetch, fetch_offline}; use cookbook::cook::fs::create_target_dir; -use cookbook::cook::package::{package, package_toml}; -use cookbook::recipe::{BuildKind, CookRecipe, Recipe}; +use cookbook::cook::package::package; +use cookbook::recipe::{CookRecipe, Recipe}; use pkg::PackageName; use cookbook::config::init_config; use cookbook::cook::cook_build::build; use termion::{color, style}; -fn cook_meta( - recipe_dir: &Path, - name: &PackageName, - recipe: &Recipe, - fetch_only: bool, -) -> Result<(), String> { - if fetch_only { - return Ok(()); - } - - let target_dir = create_target_dir(recipe_dir)?; - let empty_deps = BTreeSet::new(); - let _package_file = package_toml(&target_dir, name, recipe, &empty_deps) - .map_err(|err| format!("failed to package: {}", err))?; - - Ok(()) -} - fn cook( recipe_dir: &Path, name: &PackageName, @@ -39,12 +20,9 @@ fn cook( fetch_only: bool, is_offline: bool, ) -> Result<(), String> { - if recipe.build.kind == BuildKind::None { - return cook_meta(recipe_dir, name, recipe, fetch_only); - } let source_dir = match is_offline { - true => fetch_offline(recipe_dir, &recipe.source), - false => fetch(recipe_dir, &recipe.source), + true => fetch_offline(recipe_dir, recipe), + false => fetch(recipe_dir, recipe), } .map_err(|err| format!("failed to fetch: {}", err))?; @@ -65,7 +43,7 @@ fn cook( ) .map_err(|err| format!("failed to build: {}", err))?; - let _package_file = package(&stage_dir, &target_dir, name, recipe, &auto_deps) + package(&stage_dir, &target_dir, name, recipe, &auto_deps) .map_err(|err| format!("failed to package: {}", err))?; Ok(()) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index ec0a604f..cab4ddcd 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,9 +1,18 @@ -use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; -use std::process::{self, Command}; +use std::path::PathBuf; +use std::process; +use std::str::FromStr; use std::{env, fs}; -use anyhow::{Context, anyhow}; +use anyhow::{Context, anyhow, bail}; +use cookbook::WALK_DEPTH; +use cookbook::config::{CookConfig, get_config, init_config}; +use cookbook::cook::cook_build::build; +use cookbook::cook::fetch::{fetch, fetch_offline}; +use cookbook::cook::fs::create_target_dir; +use cookbook::cook::package::package; +use cookbook::recipe::CookRecipe; +use pkg::PackageName; +use pkg::package::PackageError; // A repo manager, to replace repo.sh @@ -36,16 +45,51 @@ struct CliConfig { repo_dir: PathBuf, sysroot_dir: PathBuf, with_package_deps: bool, - offline: bool, - nonstop: bool, all: bool, - quiet: bool, + cook: CookConfig, +} + +#[derive(PartialEq)] +enum CliCommand { + Fetch, + Cook, + Unfetch, + Clean, + Push, +} + +impl FromStr for CliCommand { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "fetch" => Ok(CliCommand::Fetch), + "cook" => Ok(CliCommand::Cook), + "unfetch" => Ok(CliCommand::Unfetch), + "clean" => Ok(CliCommand::Clean), + "push" => Ok(CliCommand::Push), + _ => Err(anyhow!("Unknown command '{}'", s)), + } + } +} + +impl ToString for CliCommand { + fn to_string(&self) -> String { + match self { + CliCommand::Fetch => "fetch".to_string(), + CliCommand::Cook => "cook".to_string(), + CliCommand::Unfetch => "unfetch".to_string(), + CliCommand::Clean => "clean".to_string(), + CliCommand::Push => "push".to_string(), + } + } } impl CliConfig { fn new() -> Result { let current_dir = env::current_dir()?; Ok(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"), sysroot_dir: if cfg!(target_os = "redox") { @@ -54,15 +98,14 @@ impl CliConfig { current_dir.join("sysroot") }, with_package_deps: false, - offline: false, - nonstop: false, + cook: get_config().cook.clone(), all: false, - quiet: false, }) } } fn main() { + init_config(); main_inner().unwrap(); } @@ -74,10 +117,29 @@ fn main_inner() -> anyhow::Result<()> { process::exit(1); } + let (config, command, recipe_names) = parse_args(args)?; + + for recipe in &recipe_names { + match command { + CliCommand::Fetch => handle_cook(recipe, &config, true, recipe.is_deps)?, + CliCommand::Cook => handle_cook(recipe, &config, false, recipe.is_deps)?, + CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, + CliCommand::Clean => handle_clean(recipe, &config, false, true)?, + CliCommand::Push => handle_push(recipe, &config)?, + } + } + + println!( + "\nCommand '{}' completed for all specified recipes.", + command.to_string(), + ); + Ok(()) +} + +fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec)> { let mut config = CliConfig::new()?; let mut command: Option = None; - let mut recipe_paths: BTreeSet = BTreeSet::new(); - + let mut recipe_names: Vec = Vec::new(); for arg in args { if arg.starts_with("--") { if let Some((key, value)) = arg.split_once('=') { @@ -93,10 +155,7 @@ fn main_inner() -> anyhow::Result<()> { } else { match arg.as_str() { "--with-package-deps" => config.with_package_deps = true, - "--offline" => config.offline = true, - "--nonstop" => config.nonstop = true, "--all" => config.all = true, - "--quiet" => config.quiet = true, _ => { eprintln!("Error: Unknown flag: {}", arg); process::exit(1); @@ -105,7 +164,6 @@ fn main_inner() -> anyhow::Result<()> { } } else if arg.starts_with('-') { match arg.as_str() { - "-q" => config.quiet = true, _ => { eprintln!("Error: Unknown flag: {}", arg); process::exit(1); @@ -116,122 +174,117 @@ fn main_inner() -> anyhow::Result<()> { command = Some(arg); } else { // Subsequent non-flag arguments are recipe names - if let Some(path) = pkg::recipes::find(&arg) { - recipe_paths.insert(path.to_owned()); - } else { - panic!("Error: recipe not found '{arg}'"); - } + recipe_names.push(arg.try_into().context("Invalid package name")?); } } - let command = command.ok_or("Error: No command specified.").unwrap(); - - if !config.all && recipe_paths.is_empty() { - panic!("Error: No recipe names provided and --all flag was not used."); - } - if config.all && !recipe_paths.is_empty() { - panic!("Error: Cannot specify recipe names when using the --all flag."); - } - - if config.all { - recipe_paths = pkg::recipes::list(""); - } - - for recipe_path in &recipe_paths { - match command.as_str() { - "fetch" => handle_fetch(recipe_path, &config)?, - "cook" => handle_cook(recipe_path, &config)?, - "unfetch" => handle_unfetch(recipe_path, &config)?, - "clean" => handle_clean(recipe_path, &config)?, - "push" => handle_push(recipe_path, &config)?, - _ => { - eprintln!("Error: Unknown command '{}'\n", command); - println!("{}", REPO_HELP_STR); - process::exit(1); - } + let command = command.ok_or(anyhow!("Error: No command specified."))?; + let command: CliCommand = str::parse(&command)?; + let recipes = if config.all { + if !recipe_names.is_empty() { + bail!("Cannot specify recipe names when using the --all flag."); } + if command == CliCommand::Cook + || command == CliCommand::Fetch + || command == CliCommand::Push + { + // because read_recipe is false below + // some recipes on wip folders are invalid anyway + bail!( + "Refusing to run an unrealistic command to {} all recipes", + command.to_string() + ); + } + + pkg::recipes::list("") + .iter() + .map(|f| CookRecipe::from_path(f, false)) + .collect::, PackageError>>()? + } else { + if recipe_names.is_empty() { + bail!("Error: No recipe names provided and --all flag was not used."); + } + if config.with_package_deps { + recipe_names = CookRecipe::get_package_deps_recursive(&recipe_names, WALK_DEPTH) + .context("failed get package deps")?; + } + + CookRecipe::get_build_deps_recursive(&recipe_names, !config.with_package_deps)? + }; + + Ok((config, command, recipes)) +} + +fn handle_cook( + recipe: &CookRecipe, + config: &CliConfig, + fetch_only: bool, + is_deps: bool, +) -> anyhow::Result<()> { + let recipe_dir = &recipe.dir; + let source_dir = match config.cook.offline { + true => fetch_offline(recipe_dir, &recipe.recipe), + false => fetch(recipe_dir, &recipe.recipe), + } + .map_err(|e| anyhow!(e))?; + + if fetch_only { + return Ok(()); } - println!( - "\nCommand '{}' completed for all specified recipes.", - command - ); + let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; + + let (stage_dir, auto_deps) = build( + recipe_dir, + &source_dir, + &target_dir, + &recipe.name, + &recipe.recipe, + config.cook.offline, + !is_deps, + ) + .map_err(|err| anyhow!("failed to build: {}", err))?; + + package( + &stage_dir, + &target_dir, + &recipe.name, + &recipe.recipe, + &auto_deps, + ) + .map_err(|err| anyhow!("failed to package: {}", err))?; + Ok(()) } -fn handle_fetch(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { - let mut cmd = Command::new("cook"); - cmd.arg("--fetch-only"); - if config.with_package_deps { - cmd.arg("--with-package-deps"); +fn handle_clean( + recipe: &CookRecipe, + _config: &CliConfig, + source: bool, + target: bool, +) -> anyhow::Result<()> { + let dir = recipe.dir.join("target"); + if dir.exists() && target { + fs::remove_dir_all(&dir).context(format!("failed to delete {}", dir.display()))?; } - if config.offline { - cmd.arg("--offline"); - } - if config.quiet { - cmd.arg("--quiet"); - } - cmd.arg(recipe_path); - let status = cmd.status().context("Failed to execute cook command")?; - if !status.success() && !config.nonstop { - return Err(anyhow!( - "Cook command failed for recipe '{}' with exit code: {}", - recipe_path.display(), - status.code().unwrap_or(1) - )); + let dir = recipe.dir.join("source"); + if dir.exists() && source { + fs::remove_dir_all(&dir).context(format!("failed to delete {}", dir.display()))?; } Ok(()) } -fn handle_cook(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { - let mut cmd = Command::new("cook"); - cmd.arg(recipe_path); - if config.with_package_deps { - cmd.arg("--with-package-deps"); - } - if config.offline { - cmd.arg("--offline"); - } - if config.quiet { - cmd.arg("--quiet"); - } - let status = cmd.status().context("Failed to execute cook command")?; - if !status.success() && !config.nonstop { - return Err(anyhow!( - "Cook command failed for recipe '{}' with exit code: {}", - recipe_path.display(), - status.code().unwrap_or(1) - )); - } - Ok(()) -} - -fn handle_unfetch(recipe_path: &Path, _config: &CliConfig) -> anyhow::Result<()> { - let dir = recipe_path.join("source"); - if dir.exists() { - fs::remove_dir_all(dir).context(format!("failed to delete {}", recipe_path.display()))?; - } - Ok(()) -} - -fn handle_clean(recipe_path: &Path, _config: &CliConfig) -> anyhow::Result<()> { - let dir = recipe_path.join("target"); - if dir.exists() { - fs::remove_dir_all(dir).context(format!("failed to delete {}", recipe_path.display()))?; - } - Ok(()) -} - -fn handle_push(recipe_path: &Path, config: &CliConfig) -> anyhow::Result<()> { +fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { let public_path = "build/id_ed25519.pub.toml"; + let archive_path = config.repo_dir.join(recipe.name.as_str()); pkgar::extract( public_path, - config.sysroot_dir.as_path(), + archive_path.as_path(), config.sysroot_dir.to_str().unwrap(), ) .context(format!( "failed to install '{}' in '{}'", - recipe_path.display(), + archive_path.display(), config.sysroot_dir.display(), )) } diff --git a/src/bin/repo_builder.rs b/src/bin/repo_builder.rs index a94123df..4748560e 100644 --- a/src/bin/repo_builder.rs +++ b/src/bin/repo_builder.rs @@ -26,6 +26,9 @@ fn main() -> Result<(), Box> { .next() .expect("Usage: repo_builder ..."); let repo_path = Path::new(&repo_dir); + if !repo_path.is_dir() { + fs::create_dir_all(repo_path)?; + } // Runtime dependencies include both `[package.dependencies]` and dynamically // linked packages discovered by auto_deps. diff --git a/src/config.rs b/src/config.rs index 54d6f837..d79848fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,58 @@ -use std::{collections::HashMap, fs, sync::OnceLock}; +use std::{collections::HashMap, env, fs, str::FromStr, sync::OnceLock}; use serde::{Deserialize, Serialize}; +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +pub struct CookConfigOpt { + /// whether to run offline + pub offline: Option, + /// whether to set jobs number instead of from nproc + pub jobs: Option, + /// 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 ignore build errors + pub nonstop: Option, + /// whether to not capture build output, + /// default is true if "tui" is false. + /// build failure still be printed anyway + pub verbose: Option, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +pub struct CookConfig { + pub offline: bool, + pub jobs: usize, + pub tui: bool, + pub nonstop: bool, + pub verbose: bool, +} + +impl From for CookConfig { + fn from(value: CookConfigOpt) -> Self { + CookConfig { + offline: value.offline.unwrap(), + jobs: value.jobs.unwrap(), + tui: value.tui.unwrap(), + nonstop: value.nonstop.unwrap(), + verbose: value.verbose.unwrap(), + } + } +} + #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] pub struct CookbookConfig { + #[serde(rename = "cook")] + cook_opt: CookConfigOpt, + #[serde(skip)] + pub cook: CookConfig, pub mirrors: HashMap, } static CONFIG: OnceLock = OnceLock::new(); pub fn init_config() { - let config: CookbookConfig = if fs::exists("cookbook.toml").unwrap_or(false) { + let mut config: CookbookConfig = if fs::exists("cookbook.toml").unwrap_or(false) { let toml_content = fs::read_to_string("cookbook.toml") .map_err(|e| format!("Unable to read config: {:?}", e)) .unwrap(); @@ -21,9 +63,47 @@ pub fn init_config() { CookbookConfig::default() }; + if config.cook_opt.tui.is_none() { + config.cook_opt.tui = Some(!env::var("CI").is_ok_and(|s| !s.is_empty())); + } + if config.cook_opt.jobs.is_none() { + config.cook_opt.jobs = Some(extract_env( + "COOKBOOK_MAKE_JOBS", + std::thread::available_parallelism() + .map(|f| usize::from(f)) + .unwrap_or(1), + )); + } + if config.cook_opt.offline.is_none() { + config.cook_opt.offline = Some(extract_env("COOKBOOK_OFFLINE", false)); + } + if config.cook_opt.verbose.is_none() { + config.cook_opt.verbose = Some(extract_env( + "COOKBOOK_VERBOSE", + !config.cook_opt.tui.unwrap(), + )); + } + if config.cook_opt.nonstop.is_none() { + config.cook_opt.nonstop = Some(extract_env("COOKBOOK_NONSTOP", false)); + } + + config.cook = CookConfig::from(config.cook_opt.clone()); + CONFIG.set(config).expect("config is initialized twice"); } +fn extract_env(key: &str, default: T) -> T { + if let Ok(e) = env::var(&key) { + str::parse(&e).unwrap_or(default) + } else { + default + } +} + +pub fn get_config() -> &'static CookbookConfig { + return CONFIG.get().expect("Configuration is not initialized"); +} + pub fn translate_mirror(original_url: &str) -> String { let config = CONFIG.get().expect("Configuration is not initialized"); diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index bdf7ab88..a4773f26 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -151,6 +151,10 @@ pub fn build( ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); + if recipe.build.kind == BuildKind::None { + // metapackages don't need to do anything here + return Ok((stage_dir, BTreeSet::new())); + } let mut dep_pkgars = BTreeSet::new(); for dependency in recipe.build.dependencies.iter() { diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index c3ce9bdc..b9a2bfc6 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -2,6 +2,7 @@ use crate::config::translate_mirror; use crate::cook::fs::*; use crate::cook::script::*; use crate::is_redox; +use crate::recipe::BuildKind; use crate::recipe::Recipe; use crate::{blake3, recipe::SourceRecipe}; use std::fs; @@ -24,14 +25,18 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result) -> Result { +pub fn fetch_offline(recipe_dir: &Path, recipe: &Recipe) -> Result { let source_dir = recipe_dir.join("source"); - match source { + if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { + // the build function doesn't need source dir exists + return Ok(source_dir); + } + match &recipe.source { Some(SourceRecipe::Path { path: _ }) | None => { - return fetch(recipe_dir, source); + return fetch(recipe_dir, recipe); } Some(SourceRecipe::SameAs { same_as: _ }) => { - return fetch(recipe_dir, source); + return fetch(recipe_dir, recipe); } Some(SourceRecipe::Git { git: _, @@ -79,17 +84,22 @@ pub fn fetch_offline(recipe_dir: &Path, source: &Option) -> Result Ok(source_dir) } -pub fn fetch(recipe_dir: &Path, source: &Option) -> Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let source_dir = recipe_dir.join("source"); - match source { + if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { + // the build function doesn't need source dir exists + return Ok(source_dir); + } + match &recipe.source { Some(SourceRecipe::SameAs { same_as }) => { - let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, same_as)?; + let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, &same_as)?; // recursively fetch - fetch(&canon_dir, &recipe.source)?; - fetch_make_symlink(&source_dir, same_as)?; + fetch(&canon_dir, &recipe)?; + fetch_make_symlink(&source_dir, &same_as)?; } Some(SourceRecipe::Path { path }) => { - if !source_dir.is_dir() || modified_dir(Path::new(path))? > modified_dir(&source_dir)? { + if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)? + { eprintln!("[DEBUG]: {} is newer than {}", path, source_dir.display()); copy_dir_all(path, &source_dir).map_err(|e| { format!( @@ -122,7 +132,7 @@ pub fn fetch(recipe_dir: &Path, source: &Option) -> Result) -> Result { if !source_dir.is_dir() { - //TODO: Don't print if build template is none or remote eprintln!( "WARNING: Recipe without source section expected source dir at '{}'", source_dir.display(), diff --git a/src/cook/package.rs b/src/cook/package.rs index aaee9fa6..a02ad9b1 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -1,8 +1,4 @@ -use std::{ - collections::BTreeSet, - env, - path::{Path, PathBuf}, -}; +use std::{collections::BTreeSet, env, path::Path}; use pkg::{Package, PackageName}; @@ -17,7 +13,13 @@ pub fn package( name: &PackageName, recipe: &Recipe, auto_deps: &BTreeSet, -) -> Result { +) -> Result<(), String> { + if recipe.build.kind == BuildKind::None { + // metapackages don't have stage dir + package_toml(target_dir, name, recipe, auto_deps)?; + return Ok(()); + } + let secret_path = "build/id_ed25519.toml"; let public_path = "build/id_ed25519.pub.toml"; if !Path::new(secret_path).is_file() || !Path::new(public_path).is_file() { @@ -58,7 +60,7 @@ pub fn package( package_toml(target_dir, name, recipe, auto_deps)?; } - Ok(package_file) + Ok(()) } pub fn package_toml( @@ -80,7 +82,8 @@ pub fn package_toml( depends, }; - serialize_and_write(&target_dir.join("stage.toml"), &package)?; + let toml_path = &target_dir.join("stage.toml"); + serialize_and_write(&toml_path, &package)?; return Ok(()); } diff --git a/src/recipe.rs b/src/recipe.rs index 99e80800..ce068a64 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,4 +1,9 @@ -use std::{collections::BTreeSet, convert::TryInto, fs, path::PathBuf}; +use std::{ + collections::BTreeSet, + convert::TryInto, + fs, + path::{Path, PathBuf}, +}; use pkg::{PackageName, package::PackageError, recipes}; use regex::Regex; @@ -146,7 +151,7 @@ pub struct PackageRecipe { } /// Everything required to build a Redox package -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Recipe { /// Specifies how to download the source for this recipe pub source: Option, @@ -158,6 +163,18 @@ pub struct Recipe { pub package: PackageRecipe, } +impl Recipe { + pub fn new(file: &PathBuf) -> Result { + if !file.is_file() { + return Err(PackageError::FileMissing(file.clone())); + } + let toml = fs::read_to_string(&file) + .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; + let recipe: Recipe = toml::from_str(&toml) + .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; + Ok(recipe) + } +} #[derive(Debug, PartialEq)] pub struct CookRecipe { pub name: PackageName, @@ -168,24 +185,7 @@ pub struct CookRecipe { } impl CookRecipe { - pub fn new( - name: impl TryInto, - ) -> Result { - let name: PackageName = name.try_into()?; - let dir = recipes::find(name.as_str()) - .ok_or_else(|| PackageError::PackageNotFound(name.clone()))?; - let file = dir.join("recipe.toml"); - if !file.is_file() { - return Err(PackageError::FileMissing(file)); - } - - let toml = fs::read_to_string(&file) - .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; - - let recipe: Recipe = toml::from_str(&toml) - .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))?; - - let dir = dir.to_path_buf(); + pub fn new(name: PackageName, dir: PathBuf, recipe: Recipe) -> Result { Ok(Self { name, dir, @@ -194,6 +194,29 @@ impl CookRecipe { }) } + pub fn from_name( + name: impl TryInto, + ) -> Result { + let name: PackageName = name.try_into()?; + let dir = recipes::find(name.as_str()) + .ok_or_else(|| PackageError::PackageNotFound(name.clone()))?; + let file = dir.join("recipe.toml"); + let recipe = Recipe::new(&file)?; + Self::new(name, dir.to_path_buf(), recipe) + } + + pub fn from_path(dir: &Path, read_recipe: bool) -> Result { + let file = dir.join("recipe.toml"); + let name: PackageName = file.file_name().unwrap().try_into()?; + let recipe = if read_recipe { + Recipe::new(&file)? + } else { + // clean/unfetch don't need to read recipe + Recipe::default() + }; + Self::new(name, dir.to_path_buf(), recipe) + } + pub fn new_recursive( names: &[PackageName], recursion: usize, @@ -204,7 +227,7 @@ impl CookRecipe { let mut recipes = Vec::new(); for name in names { - let recipe = Self::new(name.as_str())?; + let recipe = Self::from_name(name.as_str())?; let dependencies = Self::new_recursive(&recipe.recipe.build.dependencies, recursion - 1).map_err( @@ -253,7 +276,7 @@ impl CookRecipe { let mut recipes: Vec = Vec::new(); for name in names { - let recipe = Self::new(name.as_str())?; + let recipe = Self::from_name(name.as_str())?; let dependencies = Self::get_package_deps_recursive( &recipe.recipe.package.dependencies, From ab57937dd419d2cea36138c1e4485086c46e4d83 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 16:56:52 +0700 Subject: [PATCH 04/26] Implement cook TUI --- Cargo.lock | 198 ++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/bin/repo.rs | 272 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 456 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d6b149a..58737f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -374,6 +380,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.29" @@ -446,7 +467,7 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width 0.1.14", "vec_map", @@ -458,6 +479,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if 1.0.1", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.11" @@ -467,7 +502,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -602,6 +637,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -974,9 +1044,17 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1211,6 +1289,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1267,10 +1351,19 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "web-time", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -1280,6 +1373,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -1379,6 +1485,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1500,6 +1615,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbr" version = "1.1.1" @@ -1804,6 +1925,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1" +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "termion", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1871,6 +2013,7 @@ dependencies = [ "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", + "ratatui", "redox-pkg", "redoxer", "regex", @@ -2357,6 +2500,34 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2724,6 +2895,23 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -2732,9 +2920,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" diff --git a/Cargo.toml b/Cargo.toml index d82ada6b..17b6d972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" +ratatui = { version = "0.29.0", default-features = false, features = [ + "termion", +] } [dev-dependencies] tempfile = "3" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index cab4ddcd..1faa76d8 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,7 +1,10 @@ +use std::io::stdout; use std::path::PathBuf; -use std::process; use std::str::FromStr; +use std::sync::mpsc; +use std::time::Duration; use std::{env, fs}; +use std::{process, thread}; use anyhow::{Context, anyhow, bail}; use cookbook::WALK_DEPTH; @@ -13,6 +16,12 @@ use cookbook::cook::package::package; use cookbook::recipe::CookRecipe; use pkg::PackageName; use pkg::package::PackageError; +use ratatui::Terminal; +use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::prelude::TermionBackend; +use ratatui::style::{Color, Style}; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use termion::screen::{ToAlternateScreen, ToMainScreen}; // A repo manager, to replace repo.sh @@ -25,6 +34,7 @@ const REPO_HELP_STR: &str = r#" unfetch delete recipe sources clean delete recipe artifacts push extract package into sysroot + tree show tree of recipe packages common flags: --cookbook= the "recipes" folder, default to $PWD/recipes @@ -40,6 +50,7 @@ const REPO_HELP_STR: &str = r#" -q, --quiet surpress build logs unless error "#; +#[derive(Clone)] struct CliConfig { cookbook_dir: PathBuf, repo_dir: PathBuf, @@ -56,6 +67,7 @@ enum CliCommand { Unfetch, Clean, Push, + Tree, } impl FromStr for CliCommand { @@ -68,6 +80,7 @@ impl FromStr for CliCommand { "unfetch" => Ok(CliCommand::Unfetch), "clean" => Ok(CliCommand::Clean), "push" => Ok(CliCommand::Push), + "tree" => Ok(CliCommand::Tree), _ => Err(anyhow!("Unknown command '{}'", s)), } } @@ -81,6 +94,7 @@ impl ToString for CliCommand { CliCommand::Unfetch => "unfetch".to_string(), CliCommand::Clean => "clean".to_string(), CliCommand::Push => "push".to_string(), + CliCommand::Tree => "tree".to_string(), } } } @@ -119,13 +133,24 @@ fn main_inner() -> anyhow::Result<()> { let (config, command, recipe_names) = parse_args(args)?; + if command == CliCommand::Cook && config.cook.tui { + run_tui_cook(config, recipe_names)?; + return Ok(()); + } + for recipe in &recipe_names { match command { - CliCommand::Fetch => handle_cook(recipe, &config, true, recipe.is_deps)?, - CliCommand::Cook => handle_cook(recipe, &config, false, recipe.is_deps)?, + CliCommand::Fetch => { + handle_fetch(recipe, &config)?; + } + CliCommand::Cook => { + let source_dir = handle_fetch(recipe, &config)?; + handle_cook(recipe, &config, source_dir, recipe.is_deps)? + } CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, CliCommand::Clean => handle_clean(recipe, &config, false, true)?, CliCommand::Push => handle_push(recipe, &config)?, + CliCommand::Tree => todo!("tree command is WIP"), } } @@ -187,6 +212,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result<()> { +fn handle_fetch(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { true => fetch_offline(recipe_dir, &recipe.recipe), @@ -228,10 +249,16 @@ fn handle_cook( } .map_err(|e| anyhow!(e))?; - if fetch_only { - return Ok(()); - } + Ok(source_dir) +} +fn handle_cook( + recipe: &CookRecipe, + config: &CliConfig, + source_dir: PathBuf, + is_deps: bool, +) -> anyhow::Result<()> { + let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; let (stage_dir, auto_deps) = build( @@ -288,3 +315,224 @@ fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { config.sysroot_dir.display(), )) } + +#[derive(Debug, Clone, PartialEq)] +enum RecipeStatus { + Pending, + Fetching, + Fetched, + Cooking, + Done, + Failed(String), +} + +#[derive(Debug, Clone)] +enum StatusUpdate { + StartFetch(PackageName), + Fetched(PackageName), + FailFetch(PackageName, String), + StartCook(PackageName), + Cooked(PackageName), + FailCook(PackageName, String), +} + +struct TuiApp { + recipes: Vec<(CookRecipe, RecipeStatus)>, + fetch_queue: Vec, + cook_queue: Vec, + done: Vec, + failed: Vec, +} + +impl TuiApp { + fn new(recipes: Vec) -> Self { + let recipe_names = recipes.iter().map(|r| r.name.clone()).collect(); + Self { + recipes: recipes + .into_iter() + .map(|r| (r, RecipeStatus::Pending)) + .collect(), + fetch_queue: recipe_names, + cook_queue: Vec::new(), + done: Vec::new(), + failed: Vec::new(), + } + } + + // Update the state based on a message from a worker thread + fn update_status(&mut self, update: StatusUpdate) { + let (name, new_status) = match update { + StatusUpdate::StartFetch(name) => (name, RecipeStatus::Fetching), + StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), + StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), + StatusUpdate::StartCook(name) => (name, RecipeStatus::Cooking), + StatusUpdate::Cooked(name) => (name, RecipeStatus::Done), + StatusUpdate::FailCook(name, err) => (name, RecipeStatus::Failed(err)), + }; + + if let Some((_, status)) = self.recipes.iter_mut().find(|(r, _)| r.name == name) { + *status = new_status; + } + + // Re-compute the queues for display + self.fetch_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) + .map(|(r, _)| r.name.clone()) + .collect(); + self.cook_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Fetched || *s == RecipeStatus::Cooking) + .map(|(r, _)| r.name.clone()) + .collect(); + self.done = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Done) + .map(|(r, _)| r.name.clone()) + .collect(); + self.failed = self + .recipes + .iter() + .filter(|(_, s)| matches!(s, RecipeStatus::Failed(_))) + .map(|(r, _)| r.name.clone()) + .collect(); + } +} + +fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { + let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); + let (status_tx, status_rx) = mpsc::channel::(); + + // ---- Cooker Thread ---- + let cooker_config = config.clone(); + let cooker_status_tx = status_tx.clone(); + let cooker_handle = thread::spawn(move || { + for (recipe, source_dir) in work_rx { + let name = recipe.name.clone(); + let is_deps = recipe.is_deps; + cooker_status_tx + .send(StatusUpdate::StartCook(name.clone())) + .unwrap(); + + match handle_cook(&recipe, &cooker_config, source_dir, is_deps) { + Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), + Err(e) => cooker_status_tx + .send(StatusUpdate::FailCook(name, e.to_string())) + .unwrap(), + } + } + }); + + // ---- Fetcher Thread ---- + let fetcher_config = config.clone(); + let fetcher_handle = thread::spawn(move || { + for recipe in recipes { + let name = recipe.name.clone(); + status_tx + .send(StatusUpdate::StartFetch(name.clone())) + .unwrap(); + + match handle_fetch(&recipe, &fetcher_config) { + Ok(source_dir) => { + status_tx.send(StatusUpdate::Fetched(name)).unwrap(); + if work_tx.send((recipe, source_dir)).is_err() { + // Cooker thread died + break; + } + } + Err(e) => status_tx + .send(StatusUpdate::FailFetch(name, e.to_string())) + .unwrap(), + } + } + }); + + print!("{}", ToAlternateScreen); + // enable_raw_mode()?; + let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; + terminal.clear()?; + + let mut app = TuiApp::new(Vec::new()); + let total_recipes = app.recipes.len(); + let mut running = true; + + while running { + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.area()); + + // Left Pane + let fetch_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) + .map(|(r, s)| { + let style = if *s == RecipeStatus::Fetching { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + ListItem::new(r.name.as_str()).style(style) + }) + .collect(); + let fetch_list = List::new(fetch_items) + .block(Block::default().title("Fetch Queue").borders(Borders::ALL)); + f.render_widget(fetch_list, chunks[0]); + + // Right Pane + let cook_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| { + *s == RecipeStatus::Fetched + || *s == RecipeStatus::Cooking + || *s == RecipeStatus::Done + || matches!(s, RecipeStatus::Failed(_)) + }) + .map(|(r, s)| { + let style = match s { + RecipeStatus::Fetched => Style::default().fg(Color::Cyan), + RecipeStatus::Cooking => Style::default().fg(Color::Yellow), + RecipeStatus::Done => Style::default().fg(Color::Green), + RecipeStatus::Failed(_) => Style::default().fg(Color::Red), + _ => Style::default(), + }; + ListItem::new(r.name.as_str()).style(style) + }) + .collect(); + let cook_list = List::new(cook_items) + .block(Block::default().title("Cook Queue").borders(Borders::ALL)); + f.render_widget(cook_list, chunks[1]); + + let footer = Paragraph::new(format!( + "Done: {}/{} | Failed: {}", + app.done.len(), + total_recipes, + app.failed.len() + )); + f.render_widget(footer, f.area()); + })?; + + while let Ok(update) = status_rx.try_recv() { + app.update_status(update); + } + + if fetcher_handle.is_finished() && cooker_handle.is_finished() { + thread::sleep(Duration::from_secs(5)); + running = false; + } + } + + // disable_raw_mode()?; + print!("{}", ToMainScreen); + + fetcher_handle.join().unwrap(); + cooker_handle.join().unwrap(); + + Ok(()) +} From e03e843abd4b1f5c786a2ac01002cbc7255d8cf7 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 17:07:06 +0700 Subject: [PATCH 05/26] Test config --- src/config.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/config.rs b/src/config.rs index d79848fb..313acbc0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, env, fs, str::FromStr, sync::OnceLock}; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] pub struct CookConfigOpt { /// whether to run offline pub offline: Option, @@ -41,6 +42,7 @@ impl From for CookConfig { } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] pub struct CookbookConfig { #[serde(rename = "cook")] cook_opt: CookConfigOpt, @@ -155,6 +157,17 @@ mod tests { let _ = CONFIG.set(app_config); } + #[test] + fn test_parse_cook() { + let app_config: CookbookConfig = toml::from_str( + "[cook]\n\ + offline = true\n", + ) + .expect("Unable to parse test config"); + assert_eq!(app_config.cook_opt.offline, Some(true)); + assert_eq!(app_config.cook_opt.jobs, None); + } + #[test] fn test_exact_match() { setup_test_config(); From 2af2a5bd2692388da7e0ec351d4f2f5b625e7e27 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 18:41:35 +0700 Subject: [PATCH 06/26] Add command stdout pipe --- src/bin/cook.rs | 5 +- src/bin/repo.rs | 148 +++++++++++++++++++++++++++++++++-------- src/cook/cook_build.rs | 12 ++-- src/cook/fetch.rs | 54 +++++++++------ src/cook/fs.rs | 33 +++++++-- src/recipe.rs | 12 ++-- 6 files changed, 197 insertions(+), 67 deletions(-) diff --git a/src/bin/cook.rs b/src/bin/cook.rs index d5299d70..82e05744 100644 --- a/src/bin/cook.rs +++ b/src/bin/cook.rs @@ -21,8 +21,8 @@ fn cook( is_offline: bool, ) -> Result<(), String> { let source_dir = match is_offline { - true => fetch_offline(recipe_dir, recipe), - false => fetch(recipe_dir, recipe), + true => fetch_offline(recipe_dir, recipe, &None), + false => fetch(recipe_dir, recipe, &None), } .map_err(|err| format!("failed to fetch: {}", err))?; @@ -40,6 +40,7 @@ fn cook( recipe, is_offline, !is_deps, + &None, ) .map_err(|err| format!("failed to build: {}", err))?; diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 1faa76d8..bc73fed4 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,4 +1,5 @@ -use std::io::stdout; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, PipeReader, stdout}; use std::path::PathBuf; use std::str::FromStr; use std::sync::mpsc; @@ -11,7 +12,7 @@ use cookbook::WALK_DEPTH; use cookbook::config::{CookConfig, get_config, init_config}; use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; -use cookbook::cook::fs::create_target_dir; +use cookbook::cook::fs::{Stdout, create_target_dir}; use cookbook::cook::package::package; use cookbook::recipe::CookRecipe; use pkg::PackageName; @@ -20,7 +21,7 @@ use ratatui::Terminal; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; use termion::screen::{ToAlternateScreen, ToMainScreen}; // A repo manager, to replace repo.sh @@ -141,11 +142,11 @@ fn main_inner() -> anyhow::Result<()> { for recipe in &recipe_names { match command { CliCommand::Fetch => { - handle_fetch(recipe, &config)?; + handle_fetch(recipe, &config, &None)?; } CliCommand::Cook => { - let source_dir = handle_fetch(recipe, &config)?; - handle_cook(recipe, &config, source_dir, recipe.is_deps)? + let source_dir = handle_fetch(recipe, &config, &None)?; + handle_cook(recipe, &config, source_dir, recipe.is_deps, &None)? } CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, CliCommand::Clean => handle_clean(recipe, &config, false, true)?, @@ -241,11 +242,15 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result { +fn handle_fetch( + recipe: &CookRecipe, + config: &CliConfig, + logger: &Stdout, +) -> anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { - true => fetch_offline(recipe_dir, &recipe.recipe), - false => fetch(recipe_dir, &recipe.recipe), + true => fetch_offline(recipe_dir, &recipe.recipe, logger), + false => fetch(recipe_dir, &recipe.recipe, logger), } .map_err(|e| anyhow!(e))?; @@ -257,10 +262,10 @@ fn handle_cook( config: &CliConfig, source_dir: PathBuf, is_deps: bool, + logger: &Stdout, ) -> anyhow::Result<()> { let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; - let (stage_dir, auto_deps) = build( recipe_dir, &source_dir, @@ -269,6 +274,7 @@ fn handle_cook( &recipe.recipe, config.cook.offline, !is_deps, + logger, ) .map_err(|err| anyhow!("failed to build: {}", err))?; @@ -332,6 +338,7 @@ enum StatusUpdate { Fetched(PackageName), FailFetch(PackageName, String), StartCook(PackageName), + CookLog(PackageName, String), Cooked(PackageName), FailCook(PackageName, String), } @@ -342,6 +349,9 @@ struct TuiApp { cook_queue: Vec, done: Vec, failed: Vec, + active_fetch: Option, + active_cook: Option, + logs: HashMap>, } impl TuiApp { @@ -356,16 +366,36 @@ impl TuiApp { cook_queue: Vec::new(), done: Vec::new(), failed: Vec::new(), + active_fetch: None, + active_cook: None, + logs: HashMap::new(), } } // Update the state based on a message from a worker thread fn update_status(&mut self, update: StatusUpdate) { let (name, new_status) = match update { - StatusUpdate::StartFetch(name) => (name, RecipeStatus::Fetching), + StatusUpdate::StartFetch(name) => { + self.active_fetch = Some(name.clone()); + self.logs.insert(name.clone(), Vec::new()); // Clear old logs + (name.clone(), RecipeStatus::Fetching) + } StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), - StatusUpdate::StartCook(name) => (name, RecipeStatus::Cooking), + StatusUpdate::StartCook(name) => { + self.active_cook = Some(name.clone()); // Set active cook + self.logs.insert(name.clone(), Vec::new()); // Clear old logs + (name.clone(), RecipeStatus::Cooking) + } + StatusUpdate::CookLog(name, line) => { + self.logs.entry(name.clone()).or_default().push(line); + // No status change, just return the current state + if let Some((_, status)) = self.recipes.iter().find(|(r, _)| r.name == name) { + (name, status.clone()) + } else { + return; // Should not happen + } + } StatusUpdate::Cooked(name) => (name, RecipeStatus::Done), StatusUpdate::FailCook(name, err) => (name, RecipeStatus::Failed(err)), }; @@ -402,6 +432,26 @@ impl TuiApp { } } +fn spawn_log_reader( + mut pipe_reader: PipeReader, + package_name: PackageName, + status_tx: mpsc::Sender, +) { + thread::spawn(move || { + let reader = BufReader::new(&mut pipe_reader); + for line in reader.lines() { + let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); + if status_tx + .send(StatusUpdate::CookLog(package_name.clone(), line_str)) + .is_err() + { + // TUI thread hung up + break; + } + } + }); +} + fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); @@ -416,8 +466,9 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( cooker_status_tx .send(StatusUpdate::StartCook(name.clone())) .unwrap(); - - match handle_cook(&recipe, &cooker_config, source_dir, is_deps) { + let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + match handle_cook(&recipe, &cooker_config, source_dir, is_deps, &logger) { Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), Err(e) => cooker_status_tx .send(StatusUpdate::FailCook(name, e.to_string())) @@ -427,18 +478,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( }); // ---- Fetcher Thread ---- + let fetcher_recipes = recipes.clone(); let fetcher_config = config.clone(); let fetcher_handle = thread::spawn(move || { - for recipe in recipes { + for recipe in fetcher_recipes { let name = recipe.name.clone(); status_tx .send(StatusUpdate::StartFetch(name.clone())) .unwrap(); - match handle_fetch(&recipe, &fetcher_config) { + let (mut stdout_writer, mut stderr_writer) = setup_logger(&status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + + match handle_fetch(&recipe, &fetcher_config, &logger) { Ok(source_dir) => { status_tx.send(StatusUpdate::Fetched(name)).unwrap(); - if work_tx.send((recipe, source_dir)).is_err() { + if work_tx.send((recipe.clone(), source_dir)).is_err() { // Cooker thread died break; } @@ -455,15 +510,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; terminal.clear()?; - let mut app = TuiApp::new(Vec::new()); - let total_recipes = app.recipes.len(); + let mut app = TuiApp::new(recipes); + // let total_recipes = app.recipes.len(); let mut running = true; while running { terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(60), + ] + .as_ref(), + ) .split(f.area()); // Left Pane @@ -509,13 +571,34 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .block(Block::default().title("Cook Queue").borders(Borders::ALL)); f.render_widget(cook_list, chunks[1]); - let footer = Paragraph::new(format!( - "Done: {}/{} | Failed: {}", - app.done.len(), - total_recipes, - app.failed.len() - )); - f.render_widget(footer, f.area()); + let log_title = if let Some(active_name) = &app.active_cook { + format!("Build Log: {}", active_name.as_str()) + } else { + "Build Log".to_string() + }; + + let log_text: Vec = if let Some(active_name) = &app.active_cook { + app.logs + .get(active_name) + .cloned() + .unwrap_or_else(|| vec!["Waiting for logs...".to_string()]) + } else { + vec!["No active cook job.".to_string()] + }; + + let log_paragraph = Paragraph::new(log_text.join("\n")) + .block(Block::default().title(log_title).borders(Borders::ALL)) + .wrap(Wrap { trim: false }); + + f.render_widget(log_paragraph, chunks[2]); + + // let footer = Paragraph::new(format!( + // "Done: {}/{} | Failed: {}", + // app.done.len(), + // total_recipes, + // app.failed.len() + // )); + // f.render_widget(footer, f.area()); })?; while let Ok(update) = status_rx.try_recv() { @@ -536,3 +619,14 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( Ok(()) } + +fn setup_logger( + cooker_status_tx: &mpsc::Sender, + name: &PackageName, +) -> (std::io::PipeWriter, std::io::PipeWriter) { + let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); + let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); + spawn_log_reader(stdout_reader, name.clone(), cooker_status_tx.clone()); + spawn_log_reader(stderr_reader, name.clone(), cooker_status_tx.clone()); + (stdout_writer, stderr_writer) +} diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index a4773f26..016e840f 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -148,6 +148,7 @@ pub fn build( recipe: &Recipe, offline_mode: bool, check_source: bool, + logger: &Stdout, ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); @@ -296,7 +297,7 @@ pub fn build( flags_fn("COOKBOOK_MESON_FLAGS", mesonflags), ), BuildKind::Custom { script } => script.clone(), - BuildKind::Remote => return build_remote(target_dir, name, offline_mode), + BuildKind::Remote => return build_remote(target_dir, name, offline_mode, logger), BuildKind::None => "".to_owned(), }; @@ -341,7 +342,7 @@ pub fn build( "{}\n{}\n{}\n{}", BUILD_PRESCRIPT, SHARED_PRESCRIPT, script, BUILD_POSTSCRIPT ); - run_command_stdin(command, full_script.as_bytes())?; + run_command_stdin(command, full_script.as_bytes(), logger)?; // Move stage.tmp to stage atomically rename(&stage_dir_tmp, &stage_dir)?; @@ -389,6 +390,7 @@ pub fn build_remote( target_dir: &Path, name: &PackageName, offline_mode: bool, + logger: &Stdout, ) -> Result<(PathBuf, BTreeSet), String> { // download straight from remote source then declare pkg dependencies as autodeps dependency let stage_dir = target_dir.join("stage"); @@ -398,9 +400,9 @@ pub fn build_remote( let source_pubkey = target_dir.join("id_ed25519.pub.toml"); if !offline_mode { - download_wget(&get_remote_url(name, "pkgar"), &source_pkgar)?; - download_wget(&get_remote_url(name, "toml"), &source_toml)?; - download_wget(&get_pubkey_url(), &source_pubkey)?; + download_wget(&get_remote_url(name, "pkgar"), &source_pkgar, logger)?; + download_wget(&get_remote_url(name, "toml"), &source_toml, logger)?; + download_wget(&get_pubkey_url(), &source_pubkey, logger)?; } else { offline_check_exists(&source_pkgar)?; offline_check_exists(&source_toml)?; diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index b9a2bfc6..a69b3855 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -25,7 +25,11 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result Result { +pub fn fetch_offline( + recipe_dir: &Path, + recipe: &Recipe, + logger: &Stdout, +) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -33,10 +37,10 @@ pub fn fetch_offline(recipe_dir: &Path, recipe: &Recipe) -> Result { - return fetch(recipe_dir, recipe); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::SameAs { same_as: _ }) => { - return fetch(recipe_dir, recipe); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::Git { git: _, @@ -65,8 +69,8 @@ pub fn fetch_offline(recipe_dir: &Path, recipe: &Recipe) -> Result Result Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -94,7 +98,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { Some(SourceRecipe::SameAs { same_as }) => { let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, &same_as)?; // recursively fetch - fetch(&canon_dir, &recipe)?; + fetch(&canon_dir, &recipe, logger)?; fetch_make_symlink(&source_dir, &same_as)?; } Some(SourceRecipe::Path { path }) => { @@ -140,7 +144,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { command.arg("--depth").arg("1").arg("--shallow-submodules"); } command.arg(&source_dir_tmp); - run_command(command)?; + run_command(command, logger)?; // Move source.tmp to source atomically rename(&source_dir_tmp, &source_dir)?; @@ -158,13 +162,13 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("remote").arg("set-url").arg("origin").arg(git); - run_command(command)?; + run_command(command, logger)?; // Fetch origin let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("fetch").arg("origin"); - run_command(command)?; + run_command(command, logger)?; } if let Some(_upstream) = upstream { @@ -179,7 +183,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("checkout").arg(rev); - run_command(command)?; + run_command(command, logger)?; } else if !shallow_clone && !is_redox() { //TODO: complicated stuff to check and reset branch to origin //TODO: redox can't undestand this (got exit status 1) @@ -189,7 +193,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { command.env("BRANCH", branch); } command.current_dir(&source_dir); - run_command(command)?; + run_command(command, logger)?; } if !patches.is_empty() || script.is_some() { @@ -197,7 +201,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("reset").arg("--hard"); - run_command(command)?; + run_command(command, logger)?; } if !shallow_clone { @@ -205,7 +209,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("submodule").arg("sync").arg("--recursive"); - run_command(command)?; + run_command(command, logger)?; // Update submodules let mut command = Command::new("git"); @@ -215,10 +219,10 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { .arg("update") .arg("--init") .arg("--recursive"); - run_command(command)?; + run_command(command, logger)?; } - fetch_apply_patches(recipe_dir, patches, script, &source_dir)?; + fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?; } Some(SourceRecipe::Tar { tar, @@ -231,7 +235,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { while { if !source_tar.is_file() { tar_updated = true; - download_wget(&tar, &source_tar)?; + download_wget(&tar, &source_tar, logger)?; } let source_tar_blake3 = get_blake3(&source_tar, tar_updated)?; if let Some(blake3) = blake3 { @@ -268,8 +272,8 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { // Create source.tmp let source_dir_tmp = recipe_dir.join("source.tmp"); create_dir_clean(&source_dir_tmp)?; - fetch_extract_tar(source_tar, &source_dir_tmp)?; - fetch_apply_patches(recipe_dir, patches, script, &source_dir_tmp)?; + fetch_extract_tar(source_tar, &source_dir_tmp, logger)?; + fetch_apply_patches(recipe_dir, patches, script, &source_dir_tmp, logger)?; // Move source.tmp to source atomically rename(&source_dir_tmp, &source_dir)?; @@ -342,6 +346,7 @@ pub(crate) fn fetch_resolve_canon( pub(crate) fn fetch_extract_tar( source_tar: PathBuf, source_dir_tmp: &PathBuf, + logger: &Stdout, ) -> Result<(), String> { let mut command = Command::new("tar"); if is_redox() { @@ -354,7 +359,7 @@ pub(crate) fn fetch_extract_tar( command.arg(&source_tar); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip-components").arg("1"); - run_command(command)?; + run_command(command, logger)?; Ok(()) } @@ -387,6 +392,7 @@ pub(crate) fn fetch_apply_patches( patches: &Vec, script: &Option, source_dir_tmp: &PathBuf, + logger: &Stdout, ) -> Result<(), String> { for patch_name in patches { let patch_file = recipe_dir.join(patch_name); @@ -409,12 +415,16 @@ pub(crate) fn fetch_apply_patches( let mut command = Command::new("patch"); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip=1"); - run_command_stdin(command, patch.as_bytes())?; + run_command_stdin(command, patch.as_bytes(), logger)?; } Ok(if let Some(script) = script { let mut command = Command::new("bash"); command.arg("-ex"); command.current_dir(source_dir_tmp); - run_command_stdin(command, format!("{SHARED_PRESCRIPT}\n{script}").as_bytes())?; + run_command_stdin( + command, + format!("{SHARED_PRESCRIPT}\n{script}").as_bytes(), + logger, + )?; }) } diff --git a/src/cook/fs.rs b/src/cook/fs.rs index a3295d63..6a5e8b88 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -1,7 +1,7 @@ use serde::Serialize; use std::{ fs, - io::{self, Write}, + io::{self, PipeWriter, Write}, path::{Path, PathBuf}, process::{self, Command, Stdio}, time::SystemTime, @@ -146,7 +146,25 @@ pub fn rename(src: &Path, dst: &Path) -> Result<(), String> { }) } -pub fn run_command(mut command: process::Command) -> Result<(), String> { +pub type Stdout<'a> = Option<(&'a mut PipeWriter, &'a mut PipeWriter)>; + +fn pipe_to_cmd(command: &mut Command, stdout_pipe: &Stdout) -> Result<(), String> { + Ok(if let Some((stdout, stderr)) = stdout_pipe { + command.stdout::( + stdout + .try_clone() + .map_err(|e| format!("unable to clone stdout fd: {:?}", e))?, + ); + command.stderr( + stderr + .try_clone() + .map_err(|e| format!("unable to clone stderr fd: {:?}", e))?, + ); + }) +} + +pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Result<(), String> { + pipe_to_cmd(&mut command, stdout_pipe)?; let status = command .status() .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))?; @@ -161,8 +179,13 @@ pub fn run_command(mut command: process::Command) -> Result<(), String> { Ok(()) } -pub fn run_command_stdin(mut command: process::Command, stdin_data: &[u8]) -> Result<(), String> { +pub fn run_command_stdin( + mut command: process::Command, + stdin_data: &[u8], + stdout_pipe: &Stdout, +) -> Result<(), String> { command.stdin(Stdio::piped()); + pipe_to_cmd(&mut command, stdout_pipe)?; let mut child = command .spawn() @@ -217,13 +240,13 @@ pub fn offline_check_exists(path: &PathBuf) -> Result<(), String> { Ok(()) } -pub fn download_wget(url: &str, dest: &PathBuf) -> Result<(), String> { +pub fn download_wget(url: &str, dest: &PathBuf, logger: &Stdout) -> Result<(), String> { if !dest.is_file() { let dest_tmp = PathBuf::from(format!("{}.tmp", dest.display())); let mut command = Command::new("wget"); command.arg(translate_mirror(url)); command.arg("--continue").arg("-O").arg(&dest_tmp); - run_command(command)?; + run_command(command, logger)?; rename(&dest_tmp, &dest)?; } Ok(()) diff --git a/src/recipe.rs b/src/recipe.rs index ce068a64..a1ea1eef 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -15,7 +15,7 @@ use serde::{ use crate::WALK_DEPTH; /// Specifies how to download the source for a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum SourceRecipe { /// Reuse the source directory of another package @@ -88,7 +88,7 @@ impl SourceRecipe { } /// Specifies how to build a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(tag = "template")] pub enum BuildKind { /// Will not build (for meta packages) @@ -134,7 +134,7 @@ impl Default for BuildKind { } } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct BuildRecipe { #[serde(flatten, default)] pub kind: BuildKind, @@ -142,7 +142,7 @@ pub struct BuildRecipe { pub dependencies: Vec, } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct PackageRecipe { #[serde(default)] pub dependencies: Vec, @@ -151,7 +151,7 @@ pub struct PackageRecipe { } /// Everything required to build a Redox package -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct Recipe { /// Specifies how to download the source for this recipe pub source: Option, @@ -175,7 +175,7 @@ impl Recipe { Ok(recipe) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct CookRecipe { pub name: PackageName, pub dir: PathBuf, From 51a5ea0848be6950c4abd239e6cb36f18ba2e0d1 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 19:34:41 +0700 Subject: [PATCH 07/26] Add mouse handling --- Cargo.lock | 53 ++++++++++- Cargo.toml | 9 +- config.sh | 3 + fetch.sh | 1 - repo.sh | 5 +- src/bin/repo.rs | 235 ++++++++++++++++++++++++++++++++++++++---------- 6 files changed, 246 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58737f92..2319a234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -611,6 +611,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ctrlc" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" +dependencies = [ + "dispatch", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -742,6 +753,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.5" @@ -1537,6 +1554,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.1", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2007,6 +2036,7 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3 1.5.3", + "ctrlc", "ignore", "object", "pbr", @@ -3164,7 +3194,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3197,13 +3227,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3212,7 +3248,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3251,6 +3287,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 17b6d972..b4ba691b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ doctest = false [dependencies] anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar +ctrlc = { version = "3.5.0", features = ["termination"] } ignore = "0.4" object = { version = "0.36", features = ["build_core"] } pbr = "1.0.2" @@ -36,9 +37,11 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" -ratatui = { version = "0.29.0", default-features = false, features = [ - "termion", -] } + +[dependencies.ratatui] +version = "0.29.0" +default-features = false +features = ["termion"] [dev-dependencies] tempfile = "3" diff --git a/config.sh b/config.sh index c3eccb17..f60c503e 100755 --- a/config.sh +++ b/config.sh @@ -60,6 +60,9 @@ function pkgar { function cook { "$ROOT/target/release/cook" "$@" } +function repo { + "$ROOT/target/release/repo" "$@" +} function repo_builder { "$ROOT/target/release/repo_builder" "$@" } diff --git a/fetch.sh b/fetch.sh index 73da2648..16e503fd 100755 --- a/fetch.sh +++ b/fetch.sh @@ -3,4 +3,3 @@ set -e source config.sh -cook --fetch-only ${@:1} diff --git a/repo.sh b/repo.sh index ddc0e779..3ef74d17 100755 --- a/repo.sh +++ b/repo.sh @@ -18,15 +18,12 @@ do elif [ "$arg" == "--nonstop" ] then COOK_OPT+=" --nonstop" - elif [ "$arg" == "--offline" ] - then - COOK_OPT+=" --offline" else recipes+=" $arg" fi done -cook $COOK_OPT $recipes +repo cook $COOK_OPT $recipes repo="$ROOT/repo/$TARGET" mkdir -p "$repo" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index bc73fed4..d72db813 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use std::io::{BufRead, BufReader, PipeReader, stdout}; use std::path::PathBuf; use std::str::FromStr; -use std::sync::mpsc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, mpsc}; use std::time::Duration; use std::{env, fs}; use std::{process, thread}; @@ -18,11 +19,14 @@ use cookbook::recipe::CookRecipe; use pkg::PackageName; use pkg::package::PackageError; use ratatui::Terminal; -use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; -use termion::screen::{ToAlternateScreen, ToMainScreen}; +use termion::event::{Event, Key, MouseEvent}; +use termion::input::{MouseTerminal, TermRead}; +use termion::raw::RawTerminal; +use termion::screen::{AlternateScreen, ToAlternateScreen, ToMainScreen}; // A repo manager, to replace repo.sh @@ -322,6 +326,10 @@ fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { )) } +// +// ------------- TUI SPECIFIC CODE ------------------- +// + #[derive(Debug, Clone, PartialEq)] enum RecipeStatus { Pending, @@ -341,6 +349,8 @@ enum StatusUpdate { CookLog(PackageName, String), Cooked(PackageName), FailCook(PackageName, String), + FetchThreadFinished, + CookThreadFinished, } struct TuiApp { @@ -352,6 +362,15 @@ struct TuiApp { active_fetch: Option, active_cook: Option, logs: HashMap>, + log_scroll: u16, + auto_scroll: bool, + fetch_scroll: u16, + cook_scroll: u16, + fetch_complete: bool, + cook_complete: bool, + fetch_panel_rect: Option, + cook_panel_rect: Option, + log_panel_rect: Option, } impl TuiApp { @@ -369,6 +388,15 @@ impl TuiApp { active_fetch: None, active_cook: None, logs: HashMap::new(), + log_scroll: 0, + auto_scroll: true, + fetch_scroll: 0, + cook_scroll: 0, + fetch_complete: false, + cook_complete: false, + fetch_panel_rect: None, + cook_panel_rect: None, + log_panel_rect: None, } } @@ -377,14 +405,16 @@ impl TuiApp { let (name, new_status) = match update { StatusUpdate::StartFetch(name) => { self.active_fetch = Some(name.clone()); - self.logs.insert(name.clone(), Vec::new()); // Clear old logs + self.logs.insert(name.clone(), Vec::new()); + self.log_scroll = 0; + self.auto_scroll = true; (name.clone(), RecipeStatus::Fetching) } StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), StatusUpdate::StartCook(name) => { - self.active_cook = Some(name.clone()); // Set active cook - self.logs.insert(name.clone(), Vec::new()); // Clear old logs + self.active_cook = Some(name.clone()); + self.logs.insert(name.clone(), Vec::new()); (name.clone(), RecipeStatus::Cooking) } StatusUpdate::CookLog(name, line) => { @@ -396,8 +426,28 @@ impl TuiApp { return; // Should not happen } } - StatusUpdate::Cooked(name) => (name, RecipeStatus::Done), - StatusUpdate::FailCook(name, err) => (name, RecipeStatus::Failed(err)), + StatusUpdate::Cooked(name) => { + if self.active_cook.as_ref() == Some(&name) { + self.active_cook = None; + } + self.auto_scroll = true; + (name.clone(), RecipeStatus::Done) + } + StatusUpdate::FailCook(name, err) => { + if self.active_cook.as_ref() == Some(&name) { + self.active_cook = None; + } + self.auto_scroll = false; + (name.clone(), RecipeStatus::Failed(err)) + } + StatusUpdate::FetchThreadFinished => { + self.fetch_complete = true; + return; + } + StatusUpdate::CookThreadFinished => { + self.cook_complete = true; + return; + } }; if let Some((_, status)) = self.recipes.iter_mut().find(|(r, _)| r.name == name) { @@ -432,26 +482,6 @@ impl TuiApp { } } -fn spawn_log_reader( - mut pipe_reader: PipeReader, - package_name: PackageName, - status_tx: mpsc::Sender, -) { - thread::spawn(move || { - let reader = BufReader::new(&mut pipe_reader); - for line in reader.lines() { - let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); - if status_tx - .send(StatusUpdate::CookLog(package_name.clone(), line_str)) - .is_err() - { - // TUI thread hung up - break; - } - } - }); -} - fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); @@ -475,6 +505,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap(), } } + cooker_status_tx + .send(StatusUpdate::CookThreadFinished) + .unwrap_or_default(); + }); + + // ----- Input Thread ----- + let (input_tx, input_rx) = mpsc::channel::(); + let _input_handle = thread::spawn(move || { + let stdin = std::io::stdin(); + for evt in stdin.events() { + if let Ok(evt) = evt { + if input_tx.send(evt).is_err() { + return; + } + } + } }); // ---- Fetcher Thread ---- @@ -502,30 +548,37 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .send(StatusUpdate::FailFetch(name, e.to_string())) .unwrap(), } + status_tx + .send(StatusUpdate::FetchThreadFinished) + .unwrap_or_default(); } }); print!("{}", ToAlternateScreen); - // enable_raw_mode()?; let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; terminal.clear()?; let mut app = TuiApp::new(recipes); // let total_recipes = app.recipes.len(); - let mut running = true; + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); - while running { + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .context("Error setting Ctrl-C handler")?; + + while running.load(Ordering::Relaxed) { terminal.draw(|f| { + let mut constraints = Vec::new(); + if !app.fetch_complete { + constraints.push(Constraint::Length(30)); + } + constraints.push(Constraint::Length(30)); + constraints.push(Constraint::Min(20)); // Log panel always exists let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(60), - ] - .as_ref(), - ) + .constraints(constraints) .split(f.area()); // Left Pane @@ -586,19 +639,85 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( vec!["No active cook job.".to_string()] }; + let log_pane_height = chunks[2].height.saturating_sub(2); + let total_log_lines = log_text.len() as u16; + + if app.auto_scroll { + if total_log_lines > log_pane_height { + app.log_scroll = total_log_lines - log_pane_height; + } else { + app.log_scroll = 0; + } + } else { + if total_log_lines > log_pane_height { + if app.log_scroll > total_log_lines - log_pane_height { + app.log_scroll = total_log_lines - log_pane_height; + } + } else { + app.log_scroll = 0; + } + } + let log_paragraph = Paragraph::new(log_text.join("\n")) .block(Block::default().title(log_title).borders(Borders::ALL)) - .wrap(Wrap { trim: false }); + .wrap(Wrap { trim: false }) + .scroll((app.log_scroll, 0)); f.render_widget(log_paragraph, chunks[2]); - // let footer = Paragraph::new(format!( - // "Done: {}/{} | Failed: {}", - // app.done.len(), - // total_recipes, - // app.failed.len() - // )); - // f.render_widget(footer, f.area()); + while let Ok(event) = input_rx.try_recv() { + match event { + Event::Key(key) => match key { + Key::Up => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + Key::Down => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + _ => {} + }, + + Event::Mouse(mouse_event) => { + match mouse_event { + MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { + // termion is 1-based, ratatui rects are 0-based + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_sub(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_sub(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + } + MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_add(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_add(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + } + _ => {} + } + } + _ => {} + } + } })?; while let Ok(update) = status_rx.try_recv() { @@ -607,7 +726,7 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( if fetcher_handle.is_finished() && cooker_handle.is_finished() { thread::sleep(Duration::from_secs(5)); - running = false; + running.swap(false, Ordering::SeqCst); } } @@ -620,6 +739,26 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( Ok(()) } +fn spawn_log_reader( + mut pipe_reader: PipeReader, + package_name: PackageName, + status_tx: mpsc::Sender, +) { + thread::spawn(move || { + let reader = BufReader::new(&mut pipe_reader); + for line in reader.lines() { + let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); + if status_tx + .send(StatusUpdate::CookLog(package_name.clone(), line_str)) + .is_err() + { + // TUI thread hung up + break; + } + } + }); +} + fn setup_logger( cooker_status_tx: &mpsc::Sender, name: &PackageName, From 0bad6d1562134672e8107b1543c94553c4bcc91d Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 20:15:58 +0700 Subject: [PATCH 08/26] Switchable log and log fetch fix --- src/bin/repo.rs | 59 +++++++++++++++++++++++++++++++++++------------ src/cook/fetch.rs | 38 ++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index d72db813..4105f6d8 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -363,6 +363,7 @@ struct TuiApp { active_cook: Option, logs: HashMap>, log_scroll: u16, + log_view_cook: bool, auto_scroll: bool, fetch_scroll: u16, cook_scroll: u16, @@ -390,6 +391,7 @@ impl TuiApp { logs: HashMap::new(), log_scroll: 0, auto_scroll: true, + log_view_cook: false, fetch_scroll: 0, cook_scroll: 0, fetch_complete: false, @@ -442,6 +444,7 @@ impl TuiApp { } StatusUpdate::FetchThreadFinished => { self.fetch_complete = true; + self.log_view_cook = true; return; } StatusUpdate::CookThreadFinished => { @@ -548,10 +551,10 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .send(StatusUpdate::FailFetch(name, e.to_string())) .unwrap(), } - status_tx - .send(StatusUpdate::FetchThreadFinished) - .unwrap_or_default(); } + status_tx + .send(StatusUpdate::FetchThreadFinished) + .unwrap_or_default(); }); print!("{}", ToAlternateScreen); @@ -561,10 +564,10 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( let mut app = TuiApp::new(recipes); // let total_recipes = app.recipes.len(); let running = Arc::new(AtomicBool::new(true)); - let r = running.clone(); ctrlc::set_handler(move || { - r.store(false, Ordering::SeqCst); + print!("{}", ToMainScreen); + process::exit(1); }) .context("Error setting Ctrl-C handler")?; @@ -595,8 +598,11 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( ListItem::new(r.name.as_str()).style(style) }) .collect(); - let fetch_list = List::new(fetch_items) - .block(Block::default().title("Fetch Queue").borders(Borders::ALL)); + let fetch_list = List::new(fetch_items).block( + Block::default() + .title("Fetch Queue [1]") + .borders(Borders::ALL), + ); f.render_widget(fetch_list, chunks[0]); // Right Pane @@ -620,26 +626,37 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( ListItem::new(r.name.as_str()).style(style) }) .collect(); - let cook_list = List::new(cook_items) - .block(Block::default().title("Cook Queue").borders(Borders::ALL)); - f.render_widget(cook_list, chunks[1]); + let cook_list = List::new(cook_items).block( + Block::default() + .title("Cook Queue [2]") + .borders(Borders::ALL), + ); + f.render_widget(cook_list, chunks[if app.fetch_complete { 0 } else { 1 }]); - let log_title = if let Some(active_name) = &app.active_cook { + let active_name = if app.log_view_cook { + &app.active_cook + } else { + &app.active_fetch + }; + + let log_title = if let Some(active_name) = active_name { format!("Build Log: {}", active_name.as_str()) } else { "Build Log".to_string() }; - let log_text: Vec = if let Some(active_name) = &app.active_cook { + let log_text: Vec = if let Some(active_name) = active_name { app.logs .get(active_name) .cloned() .unwrap_or_else(|| vec!["Waiting for logs...".to_string()]) } else { - vec!["No active cook job.".to_string()] + vec!["No active job.".to_string()] }; - let log_pane_height = chunks[2].height.saturating_sub(2); + let log_pane_height = chunks[if app.fetch_complete { 1 } else { 2 }] + .height + .saturating_sub(2); let total_log_lines = log_text.len() as u16; if app.auto_scroll { @@ -663,11 +680,23 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .wrap(Wrap { trim: false }) .scroll((app.log_scroll, 0)); - f.render_widget(log_paragraph, chunks[2]); + f.render_widget( + log_paragraph, + chunks[if app.fetch_complete { 1 } else { 2 }], + ); while let Ok(event) = input_rx.try_recv() { match event { Event::Key(key) => match key { + Key::Char('\t') => { + app.log_view_cook = !app.log_view_cook; + } + Key::Char('1') => { + app.log_view_cook = false; + } + Key::Char('2') => { + app.log_view_cook = true; + } Key::Up => { app.auto_scroll = false; app.log_scroll = app.log_scroll.saturating_sub(1); diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index a69b3855..60df1314 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -9,6 +9,21 @@ 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) @@ -104,7 +119,12 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result { if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)? { - eprintln!("[DEBUG]: {} is newer than {}", path, source_dir.display()); + log_warn!( + logger, + "[DEBUG]: {} is newer than {}", + path, + source_dir.display() + ); copy_dir_all(path, &source_dir).map_err(|e| { format!( "Couldn't copy source from {} to {}: {}", @@ -245,7 +265,10 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result Result Result Result { if !source_dir.is_dir() { - eprintln!( + log_warn!( + logger, "WARNING: Recipe without source section expected source dir at '{}'", source_dir.display(), ); From ff3cd11099a18fcfebf28b8c370399275b97868f Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 05:38:54 +0700 Subject: [PATCH 09/26] Now mostly usable --- Cargo.lock | 53 +--- Cargo.toml | 3 +- clean.sh | 10 +- src/bin/repo.rs | 559 ++++++++++++++++++++++++++++++----------- src/cook/cook_build.rs | 44 +++- src/cook/fetch.rs | 4 +- unfetch.sh | 11 +- 7 files changed, 457 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2319a234..58737f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -611,17 +611,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ctrlc" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" -dependencies = [ - "dispatch", - "nix", - "windows-sys 0.61.2", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -753,12 +742,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "displaydoc" version = "0.2.5" @@ -1554,18 +1537,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", - "cfg_aliases", - "libc", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2036,7 +2007,6 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3 1.5.3", - "ctrlc", "ignore", "object", "pbr", @@ -3194,7 +3164,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] @@ -3227,19 +3197,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3248,7 +3212,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3287,15 +3251,6 @@ dependencies = [ "windows-targets 0.53.2", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index b4ba691b..8b28c1c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "redox_cookbook" version = "0.1.0" authors = ["Jeremy Soller "] edition = "2024" -default-run = "cook" +default-run = "repo" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,7 +23,6 @@ doctest = false [dependencies] anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar -ctrlc = { version = "3.5.0", features = ["termination"] } ignore = "0.4" object = { version = "0.36", features = ["build_core"] } pbr = "1.0.2" diff --git a/clean.sh b/clean.sh index 6f7d0b0f..211fcb7c 100755 --- a/clean.sh +++ b/clean.sh @@ -5,15 +5,9 @@ source config.sh if [ $# = 0 ] then - recipes="$(list_recipes --short)" + recipes="--all" else recipes="$@" fi -for recipe_name in $recipes -do - recipe_path=`find_recipe $recipe_name` - - echo -e "\033[01;38;5;215mcook - clean $recipe_name\033[0m" - rm -rf "${ROOT}/$recipe_path/target/${TARGET}" -done +repo clean $recipes diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 4105f6d8..8f867db7 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; -use std::io::{BufRead, BufReader, PipeReader, stdout}; +use std::collections::{HashMap, VecDeque}; +use std::io::{BufRead, BufReader, PipeReader, Write, stderr, stdin, stdout}; use std::path::PathBuf; +use std::process::Command; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, mpsc}; use std::time::Duration; use std::{env, fs}; @@ -22,11 +23,13 @@ use ratatui::Terminal; use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}; use termion::event::{Event, Key, MouseEvent}; -use termion::input::{MouseTerminal, TermRead}; -use termion::raw::RawTerminal; -use termion::screen::{AlternateScreen, ToAlternateScreen, ToMainScreen}; +use termion::input::TermRead; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use termion::{color, style}; // A repo manager, to replace repo.sh @@ -125,7 +128,10 @@ impl CliConfig { fn main() { init_config(); - main_inner().unwrap(); + if let Err(e) = main_inner() { + eprintln!("{:?}", e); + process::exit(1); + }; } fn main_inner() -> anyhow::Result<()> { @@ -139,7 +145,19 @@ fn main_inner() -> anyhow::Result<()> { let (config, command, recipe_names) = parse_args(args)?; if command == CliCommand::Cook && config.cook.tui { - run_tui_cook(config, recipe_names)?; + if let Some(e) = run_tui_cook(config, recipe_names)? { + let _ = stderr().write(e.as_bytes()); + let _ = stderr().write(b"\n\n"); + return Err(anyhow!("Execution has failed")); + } else { + eprintln!( + "{}{}cook - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(215)), + color::Fg(color::Reset), + style::Reset, + ); + } return Ok(()); } @@ -343,27 +361,32 @@ enum RecipeStatus { #[derive(Debug, Clone)] enum StatusUpdate { StartFetch(PackageName), - Fetched(PackageName), - FailFetch(PackageName, String), + Fetched(CookRecipe), + FailFetch(CookRecipe, String), StartCook(PackageName), - CookLog(PackageName, String), - Cooked(PackageName), - FailCook(PackageName, String), + Cooked(CookRecipe), + FailCook(CookRecipe, String), + PushLog(PackageName, String), FetchThreadFinished, CookThreadFinished, } +#[derive(PartialEq)] +enum JobType { + Fetch, + Cook, +} + struct TuiApp { recipes: Vec<(CookRecipe, RecipeStatus)>, - fetch_queue: Vec, - cook_queue: Vec, + fetch_queue: VecDeque, + cook_queue: VecDeque, done: Vec, - failed: Vec, active_fetch: Option, active_cook: Option, logs: HashMap>, log_scroll: u16, - log_view_cook: bool, + log_view_job: JobType, auto_scroll: bool, fetch_scroll: u16, cook_scroll: u16, @@ -372,26 +395,27 @@ struct TuiApp { fetch_panel_rect: Option, cook_panel_rect: Option, log_panel_rect: Option, + prompt: Option, + dump_logs_on_exit: Option, } impl TuiApp { fn new(recipes: Vec) -> Self { - let recipe_names = recipes.iter().map(|r| r.name.clone()).collect(); Self { recipes: recipes - .into_iter() + .iter() + .cloned() .map(|r| (r, RecipeStatus::Pending)) .collect(), - fetch_queue: recipe_names, - cook_queue: Vec::new(), + fetch_queue: recipes.iter().cloned().map(|r| r.clone()).collect(), + cook_queue: VecDeque::new(), done: Vec::new(), - failed: Vec::new(), active_fetch: None, active_cook: None, logs: HashMap::new(), log_scroll: 0, auto_scroll: true, - log_view_cook: false, + log_view_job: JobType::Fetch, fetch_scroll: 0, cook_scroll: 0, fetch_complete: false, @@ -399,6 +423,8 @@ impl TuiApp { fetch_panel_rect: None, cook_panel_rect: None, log_panel_rect: None, + prompt: None, + dump_logs_on_exit: None, } } @@ -412,14 +438,17 @@ impl TuiApp { self.auto_scroll = true; (name.clone(), RecipeStatus::Fetching) } - StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), - StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), + StatusUpdate::Fetched(recipe) => (recipe.name.clone(), RecipeStatus::Fetched), + StatusUpdate::FailFetch(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + (recipe.name.clone(), RecipeStatus::Failed(err)) + } StatusUpdate::StartCook(name) => { self.active_cook = Some(name.clone()); self.logs.insert(name.clone(), Vec::new()); (name.clone(), RecipeStatus::Cooking) } - StatusUpdate::CookLog(name, line) => { + StatusUpdate::PushLog(name, line) => { self.logs.entry(name.clone()).or_default().push(line); // No status change, just return the current state if let Some((_, status)) = self.recipes.iter().find(|(r, _)| r.name == name) { @@ -428,23 +457,21 @@ impl TuiApp { return; // Should not happen } } - StatusUpdate::Cooked(name) => { - if self.active_cook.as_ref() == Some(&name) { + StatusUpdate::Cooked(recipe) => { + if self.active_cook.as_ref() == Some(&recipe.name) { self.active_cook = None; } self.auto_scroll = true; - (name.clone(), RecipeStatus::Done) + (recipe.name.clone(), RecipeStatus::Done) } - StatusUpdate::FailCook(name, err) => { - if self.active_cook.as_ref() == Some(&name) { - self.active_cook = None; - } - self.auto_scroll = false; - (name.clone(), RecipeStatus::Failed(err)) + StatusUpdate::FailCook(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + + (recipe.name.clone(), RecipeStatus::Failed(err)) } StatusUpdate::FetchThreadFinished => { self.fetch_complete = true; - self.log_view_cook = true; + self.log_view_job = JobType::Cook; return; } StatusUpdate::CookThreadFinished => { @@ -461,14 +488,14 @@ impl TuiApp { self.fetch_queue = self .recipes .iter() - .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) - .map(|(r, _)| r.name.clone()) + .filter(|(_, s)| *s == RecipeStatus::Pending) + .map(|(r, _)| r.clone()) .collect(); self.cook_queue = self .recipes .iter() - .filter(|(_, s)| *s == RecipeStatus::Fetched || *s == RecipeStatus::Cooking) - .map(|(r, _)| r.name.clone()) + .filter(|(_, s)| *s == RecipeStatus::Fetched) + .map(|(r, _)| (r.clone())) .collect(); self.done = self .recipes @@ -476,24 +503,22 @@ impl TuiApp { .filter(|(_, s)| *s == RecipeStatus::Done) .map(|(r, _)| r.name.clone()) .collect(); - self.failed = self - .recipes - .iter() - .filter(|(_, s)| matches!(s, RecipeStatus::Failed(_))) - .map(|(r, _)| r.name.clone()) - .collect(); } } -fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { +fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); + let running = Arc::new(AtomicBool::new(true)); + let prompting = Arc::new(AtomicU32::new(0)); + // ---- Cooker Thread ---- let cooker_config = config.clone(); let cooker_status_tx = status_tx.clone(); + let cooker_prompting = prompting.clone(); let cooker_handle = thread::spawn(move || { - for (recipe, source_dir) in work_rx { + 'done: for (recipe, source_dir) in work_rx { let name = recipe.name.clone(); let is_deps = recipe.is_deps; cooker_status_tx @@ -501,11 +526,51 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap(); let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); let logger = Some((&mut stdout_writer, &mut stderr_writer)); - match handle_cook(&recipe, &cooker_config, source_dir, is_deps, &logger) { - Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), - Err(e) => cooker_status_tx - .send(StatusUpdate::FailCook(name, e.to_string())) - .unwrap(), + 'again: loop { + match handle_cook( + &recipe, + &cooker_config, + source_dir.clone(), + is_deps, + &logger, + ) { + Ok(()) => { + cooker_status_tx + .send(StatusUpdate::Cooked(recipe)) + .unwrap_or_default(); + break; + } + Err(e) => { + cooker_status_tx + .send(StatusUpdate::FailCook(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !cooker_config.cook.nonstop { + while cooker_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + cooker_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match cooker_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } + } } } cooker_status_tx @@ -513,11 +578,17 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap_or_default(); }); + let mstdin = stdin(); + let mstdout = stdout() + .into_raw_mode() + .unwrap() + .into_alternate_screen() + .unwrap(); + // ----- Input Thread ----- let (input_tx, input_rx) = mpsc::channel::(); let _input_handle = thread::spawn(move || { - let stdin = std::io::stdin(); - for evt in stdin.events() { + for evt in mstdin.events() { if let Ok(evt) = evt { if input_tx.send(evt).is_err() { return; @@ -528,28 +599,62 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( // ---- Fetcher Thread ---- let fetcher_recipes = recipes.clone(); + let fetcher_status_tx = status_tx.clone(); let fetcher_config = config.clone(); + let fetcher_prompting = prompting.clone(); let fetcher_handle = thread::spawn(move || { - for recipe in fetcher_recipes { + 'done: for recipe in fetcher_recipes { let name = recipe.name.clone(); - status_tx + fetcher_status_tx .send(StatusUpdate::StartFetch(name.clone())) .unwrap(); - let (mut stdout_writer, mut stderr_writer) = setup_logger(&status_tx, &name); + let (mut stdout_writer, mut stderr_writer) = setup_logger(&fetcher_status_tx, &name); let logger = Some((&mut stdout_writer, &mut stderr_writer)); - match handle_fetch(&recipe, &fetcher_config, &logger) { - Ok(source_dir) => { - status_tx.send(StatusUpdate::Fetched(name)).unwrap(); - if work_tx.send((recipe.clone(), source_dir)).is_err() { - // Cooker thread died + 'again: loop { + match handle_fetch(&recipe, &fetcher_config, &logger) { + Ok(source_dir) => { + fetcher_status_tx + .send(StatusUpdate::Fetched(recipe.clone())) + .unwrap(); + if work_tx.send((recipe.clone(), source_dir)).is_err() { + // Cooker thread died + break 'done; + } break; } + Err(e) => { + fetcher_status_tx + .send(StatusUpdate::FailFetch(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !fetcher_config.cook.nonstop { + while fetcher_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + fetcher_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match fetcher_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } } - Err(e) => status_tx - .send(StatusUpdate::FailFetch(name, e.to_string())) - .unwrap(), } } status_tx @@ -557,28 +662,19 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap_or_default(); }); - print!("{}", ToAlternateScreen); let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; terminal.clear()?; let mut app = TuiApp::new(recipes); - // let total_recipes = app.recipes.len(); - let running = Arc::new(AtomicBool::new(true)); - ctrlc::set_handler(move || { - print!("{}", ToMainScreen); - process::exit(1); - }) - .context("Error setting Ctrl-C handler")?; - - while running.load(Ordering::Relaxed) { + while running.load(Ordering::SeqCst) { terminal.draw(|f| { let mut constraints = Vec::new(); if !app.fetch_complete { constraints.push(Constraint::Length(30)); } constraints.push(Constraint::Length(30)); - constraints.push(Constraint::Min(20)); // Log panel always exists + constraints.push(Constraint::Min(20)); let chunks = Layout::default() .direction(Direction::Horizontal) .constraints(constraints) @@ -633,7 +729,7 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( ); f.render_widget(cook_list, chunks[if app.fetch_complete { 0 } else { 1 }]); - let active_name = if app.log_view_cook { + let active_name = if app.log_view_job == JobType::Cook { &app.active_cook } else { &app.active_fetch @@ -667,8 +763,9 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( } } else { if total_log_lines > log_pane_height { - if app.log_scroll > total_log_lines - log_pane_height { + if app.log_scroll >= total_log_lines - log_pane_height { app.log_scroll = total_log_lines - log_pane_height; + app.auto_scroll = true; } } else { app.log_scroll = 0; @@ -685,66 +782,21 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( chunks[if app.fetch_complete { 1 } else { 2 }], ); + if let Some(prompt) = &app.prompt { + draw_prompt(f, prompt); + } + while let Ok(event) = input_rx.try_recv() { - match event { - Event::Key(key) => match key { - Key::Char('\t') => { - app.log_view_cook = !app.log_view_cook; - } - Key::Char('1') => { - app.log_view_cook = false; - } - Key::Char('2') => { - app.log_view_cook = true; - } - Key::Up => { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_sub(1); - } - Key::Down => { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_add(1); - } - _ => {} - }, - - Event::Mouse(mouse_event) => { - match mouse_event { - MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { - // termion is 1-based, ratatui rects are 0-based - let pos = Position { - x: x.saturating_sub(1), - y: y.saturating_sub(1), - }; - - if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { - app.fetch_scroll = app.fetch_scroll.saturating_sub(1); - } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { - app.cook_scroll = app.cook_scroll.saturating_sub(1); - } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_sub(1); - } - } - MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { - let pos = Position { - x: x.saturating_sub(1), - y: y.saturating_sub(1), - }; - - if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { - app.fetch_scroll = app.fetch_scroll.saturating_add(1); - } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { - app.cook_scroll = app.cook_scroll.saturating_add(1); - } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_add(1); - } - } - _ => {} + if app.prompt.is_some() { + if let Some(res) = handle_prompt_input(event, &mut app, &running) { + prompting.swap(res as u32, Ordering::SeqCst); + if res == PromptOption::Exit { + app.dump_logs_on_exit = Some(log_text.join("\n")) } + app.prompt = None; } - _ => {} + } else { + handle_main_event(&mut app, event); } } })?; @@ -753,19 +805,194 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( app.update_status(update); } - if fetcher_handle.is_finished() && cooker_handle.is_finished() { - thread::sleep(Duration::from_secs(5)); + if app.cook_complete { running.swap(false, Ordering::SeqCst); } } - // disable_raw_mode()?; - print!("{}", ToMainScreen); + drop(mstdout); + let _ = stdout().flush(); fetcher_handle.join().unwrap(); cooker_handle.join().unwrap(); - Ok(()) + Ok(app.dump_logs_on_exit) +} + +fn handle_main_event(app: &mut TuiApp, event: Event) { + match event { + Event::Key(key) => match key { + Key::Char('1') => { + app.log_view_job = JobType::Fetch; + } + Key::Char('2') => { + app.log_view_job = JobType::Cook; + } + 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()) + .spawn() + .expect("unable to spawn pkill"); + } + Key::Up => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + Key::Down => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + Key::PageUp => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(20); + } + Key::PageDown => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(20); + } + Key::End => { + app.auto_scroll = true; + } + Key::Home => { + app.auto_scroll = false; + app.log_scroll = 0; + } + _ => {} + }, + + Event::Mouse(mouse_event) => { + match mouse_event { + MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { + // termion is 1-based, ratatui rects are 0-based + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_sub(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_sub(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + } + MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_add(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_add(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + } + _ => {} + } + } + _ => {} + } +} + +fn handle_prompt_input( + event: Event, + app: &mut TuiApp, + running: &Arc, +) -> Option { + if let Some(prompt) = &mut app.prompt { + match event { + Event::Key(key) => match key { + Key::Char('q') | Key::Ctrl('c') | Key::Esc => { + // Treat as "Exit" + running.store(false, Ordering::SeqCst); + return Some(PromptOption::Exit); + } + Key::Left | Key::BackTab => prompt.prev(), + Key::Right | Key::Char('\t') => prompt.next(), + Key::Char('\n') => { + let prompt = app.prompt.take().unwrap(); + if prompt.selected == PromptOption::Exit { + running.store(false, Ordering::SeqCst); + } + return Some(prompt.selected); + } + _ => {} + }, + _ => {} // Ignore mouse events + } + } + None +} + +fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { + let title = format!(" FAILURE in {} ", prompt.recipe.name); + let mut error_text = prompt.error.clone(); + if error_text.len() > 100 { + error_text = error_text[0..100].to_string() + ".."; + } + + // Style for options + let retry_style = if prompt.selected == PromptOption::Retry { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let skip_style = if prompt.selected == PromptOption::Skip { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let exit_style = if prompt.selected == PromptOption::Exit { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + + let text = vec![ + Line::from(error_text).style(Style::default().fg(Color::Yellow)), + Line::from(""), + Line::from(vec![ + Span::styled(" [Retry] ", retry_style), + Span::raw(" "), + Span::styled(" [Skip] ", skip_style), + Span::raw(" "), + Span::styled(" [Exit] ", exit_style), + ]), + ]; + + let block = Block::default() + .title(Span::styled( + title, + Style::default().fg(Color::White).bg(Color::Red), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let paragraph = Paragraph::new(text) + .block(block) + .alignment(ratatui::layout::Alignment::Center) + .wrap(Wrap { trim: true }); + + let area = f.area(); + let popup_area = Rect { + x: area.width / 4, + y: area.height / 3, + width: area.width / 2, + height: 10, + }; + + f.render_widget(Clear, popup_area); // Clear the background + f.render_widget(paragraph, popup_area); } fn spawn_log_reader( @@ -778,7 +1005,7 @@ fn spawn_log_reader( for line in reader.lines() { let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); if status_tx - .send(StatusUpdate::CookLog(package_name.clone(), line_str)) + .send(StatusUpdate::PushLog(package_name.clone(), line_str)) .is_err() { // TUI thread hung up @@ -789,12 +1016,52 @@ fn spawn_log_reader( } fn setup_logger( - cooker_status_tx: &mpsc::Sender, + status_tx: &mpsc::Sender, name: &PackageName, ) -> (std::io::PipeWriter, std::io::PipeWriter) { let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); - spawn_log_reader(stdout_reader, name.clone(), cooker_status_tx.clone()); - spawn_log_reader(stderr_reader, name.clone(), cooker_status_tx.clone()); + spawn_log_reader(stdout_reader, name.clone(), status_tx.clone()); + spawn_log_reader(stderr_reader, name.clone(), status_tx.clone()); (stdout_writer, stderr_writer) } + +#[derive(PartialEq, Clone, Copy)] +#[repr(u32)] +enum PromptOption { + Retry = 2, + Skip, + Exit, +} + +struct FailurePrompt { + recipe: CookRecipe, + error: String, + selected: PromptOption, +} + +impl FailurePrompt { + fn new(recipe: CookRecipe, error: String) -> Self { + Self { + recipe, + error, + selected: PromptOption::Exit, + } + } + + fn next(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Skip, + PromptOption::Skip => PromptOption::Exit, + PromptOption::Exit => PromptOption::Retry, + } + } + + fn prev(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Exit, + PromptOption::Skip => PromptOption::Retry, + PromptOption::Exit => PromptOption::Skip, + } + } +} diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index 016e840f..a9c92164 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -21,9 +21,25 @@ use crate::is_redox; 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( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, + logger: &Stdout, ) -> BTreeSet { let mut paths = BTreeSet::new(); let mut visited = BTreeSet::new(); @@ -43,7 +59,10 @@ fn auto_deps( }; if visited.contains(&dir) { #[cfg(debug_assertions)] - eprintln!("DEBUG: auto_deps => Skipping `{dir:?}` (already visited)"); + log_warn!( + logger, + "DEBUG: auto_deps => Skipping `{dir:?}` (already visited)" + ); continue; } assert!( @@ -90,7 +109,7 @@ fn auto_deps( continue; }; if let Ok(relative_path) = path.strip_prefix(stage_dir) { - eprintln!("DEBUG: {} needs {}", relative_path.display(), name); + log_warn!(logger, "DEBUG: {} needs {}", relative_path.display(), name); } needed.insert(name.to_string()); } @@ -124,7 +143,7 @@ fn auto_deps( continue; }; if needed.contains(child_name) { - eprintln!("DEBUG: {} provides {}", dep, child_name); + log_warn!(logger, "DEBUG: {} provides {}", dep, child_name); deps.insert(dep.clone()); missing.remove(child_name); } @@ -134,7 +153,7 @@ fn auto_deps( } for name in missing { - eprintln!("WARN: {} missing", name); + log_warn!(logger, "WARN: {} missing", name); } deps @@ -174,7 +193,7 @@ pub fn build( } if stage_dir.exists() && !check_source { - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; return Ok((stage_dir, auto_deps)); } @@ -190,7 +209,8 @@ pub fn build( if sysroot_dir.is_dir() { let sysroot_modified = modified_dir(&sysroot_dir)?; if sysroot_modified < source_modified || sysroot_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), sysroot_dir.display() @@ -239,7 +259,8 @@ pub fn build( if stage_dir.is_dir() { let stage_modified = modified_dir(&stage_dir)?; if stage_modified < source_modified || stage_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), stage_dir.display() @@ -318,7 +339,7 @@ pub fn build( } else { let cookbook_redoxer = Path::new("target/release/cookbook_redoxer") .canonicalize() - .unwrap(); + .unwrap_or(PathBuf::from("/bin/false")); let mut command = Command::new(&cookbook_redoxer); command.arg("env").arg("bash").arg("-ex"); command.env("COOKBOOK_REDOXER", &cookbook_redoxer); @@ -348,7 +369,7 @@ pub fn build( rename(&stage_dir_tmp, &stage_dir)?; } - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; Ok((stage_dir, auto_deps)) } @@ -358,6 +379,7 @@ fn build_auto_deps( target_dir: &Path, stage_dir: &PathBuf, dep_pkgars: BTreeSet<(PackageName, PathBuf)>, + logger: &Stdout, ) -> Result, String> { let auto_deps_path = target_dir.join("auto_deps.toml"); if auto_deps_path.is_file() && modified(&auto_deps_path)? < modified(stage_dir)? { @@ -371,7 +393,7 @@ fn build_auto_deps( toml::from_str(&toml_content).map_err(|_| "failed to deserialize cached auto_deps")?; wrapper.packages } else { - let packages = auto_deps(stage_dir, &dep_pkgars); + let packages = auto_deps(stage_dir, &dep_pkgars, logger); let wrapper = AutoDeps { packages }; serialize_and_write(&auto_deps_path, &wrapper)?; wrapper.packages @@ -475,7 +497,7 @@ mod tests { "Expected a loop where {dir:?} points to {root:?}" ); - let entries = auto_deps(root, &Default::default()); + let entries = auto_deps(root, &Default::default(), &None); assert!( entries.is_empty(), "auto_deps shouldn't have yielded any libraries" diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 60df1314..6295cae7 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -76,7 +76,7 @@ pub fn fetch_offline( }) => { if !source_dir.is_dir() { let source_tar = recipe_dir.join("source.tar"); - let source_tar_blake3 = get_blake3(&source_tar, true)?; + let source_tar_blake3 = get_blake3(&source_tar, true && logger.is_none())?; if source_tar.exists() { if let Some(blake3) = blake3 { if source_tar_blake3 != *blake3 { @@ -257,7 +257,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result Date: Sat, 25 Oct 2025 05:44:35 +0700 Subject: [PATCH 10/26] Fix clean --- src/recipe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recipe.rs b/src/recipe.rs index a1ea1eef..98ca4360 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -207,7 +207,7 @@ impl CookRecipe { pub fn from_path(dir: &Path, read_recipe: bool) -> Result { let file = dir.join("recipe.toml"); - let name: PackageName = file.file_name().unwrap().try_into()?; + let name: PackageName = dir.file_name().unwrap().try_into()?; let recipe = if read_recipe { Recipe::new(&file)? } else { From be2a4ddd0a9f9d3ca88ab6eede4387a3e871d22c Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 12:06:54 +0700 Subject: [PATCH 11/26] Tell what recipe error, optimized rendering --- src/bin/repo.rs | 160 ++++++++++++++++++++++++++------------------ src/cook/package.rs | 4 ++ 2 files changed, 99 insertions(+), 65 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 8f867db7..378118fb 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, mpsc}; use std::time::Duration; -use std::{env, fs}; +use std::{cmp, env, fs}; use std::{process, thread}; use anyhow::{Context, anyhow, bail}; @@ -145,15 +145,23 @@ fn main_inner() -> anyhow::Result<()> { let (config, command, recipe_names) = parse_args(args)?; if command == CliCommand::Cook && config.cook.tui { - if let Some(e) = run_tui_cook(config, recipe_names)? { + if let Some((name, e)) = run_tui_cook(config, recipe_names)? { let _ = stderr().write(e.as_bytes()); let _ = stderr().write(b"\n\n"); + eprintln!( + "{}{}cook - failed at {}{}{}", + style::Bold, + color::Fg(color::AnsiValue(196)), + name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); return Err(anyhow!("Execution has failed")); } else { eprintln!( "{}{}cook - successful{}{}", style::Bold, - color::Fg(color::AnsiValue(215)), + color::Fg(color::AnsiValue(46)), color::Fg(color::Reset), style::Reset, ); @@ -274,7 +282,7 @@ fn handle_fetch( true => fetch_offline(recipe_dir, &recipe.recipe, logger), false => fetch(recipe_dir, &recipe.recipe, logger), } - .map_err(|e| anyhow!(e))?; + .map_err(|e| anyhow!("failed to fetch: {:?}", e))?; Ok(source_dir) } @@ -298,7 +306,7 @@ fn handle_cook( !is_deps, logger, ) - .map_err(|err| anyhow!("failed to build: {}", err))?; + .map_err(|err| anyhow!("failed to build: {:?}", err))?; package( &stage_dir, @@ -307,7 +315,7 @@ fn handle_cook( &recipe.recipe, &auto_deps, ) - .map_err(|err| anyhow!("failed to package: {}", err))?; + .map_err(|err| anyhow!("failed to package: {:?}", err))?; Ok(()) } @@ -385,18 +393,18 @@ struct TuiApp { active_fetch: Option, active_cook: Option, logs: HashMap>, - log_scroll: u16, + log_scroll: usize, log_view_job: JobType, auto_scroll: bool, - fetch_scroll: u16, - cook_scroll: u16, + fetch_scroll: usize, + cook_scroll: usize, fetch_complete: bool, cook_complete: bool, fetch_panel_rect: Option, cook_panel_rect: Option, log_panel_rect: Option, prompt: Option, - dump_logs_on_exit: Option, + dump_logs_on_exit: Option<(PackageName, String)>, } impl TuiApp { @@ -506,7 +514,10 @@ impl TuiApp { } } -fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result> { +fn run_tui_cook( + config: CliConfig, + recipes: Vec, +) -> anyhow::Result> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); @@ -729,11 +740,7 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result) -> anyhow::Result = if let Some(active_name) = active_name { - app.logs - .get(active_name) - .cloned() - .unwrap_or_else(|| vec!["Waiting for logs...".to_string()]) - } else { - vec!["No active job.".to_string()] - }; + let mut enable_auto_scroll = false; - let log_pane_height = chunks[if app.fetch_complete { 1 } else { 2 }] - .height - .saturating_sub(2); - let total_log_lines = log_text.len() as u16; + let log_lines: Vec = if let Some(log_text) = log_text + && log_text.len() > 0 + { + let log_pane_height = chunks[if app.fetch_complete { 1 } else { 2 }] + .height + .saturating_sub(2) as usize; + let total_log_lines = log_text.len() as usize; - if app.auto_scroll { - if total_log_lines > log_pane_height { - app.log_scroll = total_log_lines - log_pane_height; - } else { - app.log_scroll = 0; - } - } else { - if total_log_lines > log_pane_height { - if app.log_scroll >= total_log_lines - log_pane_height { - app.log_scroll = total_log_lines - log_pane_height; - app.auto_scroll = true; + let start = if app.auto_scroll { + if total_log_lines > log_pane_height { + total_log_lines - log_pane_height + } else { + 0 } } else { - app.log_scroll = 0; - } - } + if total_log_lines > log_pane_height { + if app.log_scroll >= total_log_lines - log_pane_height { + enable_auto_scroll = true; + total_log_lines - log_pane_height + } else { + app.log_scroll + } + } else { + 0 + } + }; - let log_paragraph = Paragraph::new(log_text.join("\n")) + let end = cmp::min(log_pane_height + start, total_log_lines - 1); + + log_text[start..end] + .iter() + .map(|s| Line::from(s.clone())) + .collect() + } else { + vec![Line::from("No logs yet")] + }; + + let log_paragraph = Paragraph::new(log_lines) .block(Block::default().title(log_title).borders(Borders::ALL)) - .wrap(Wrap { trim: false }) - .scroll((app.log_scroll, 0)); + .wrap(Wrap { trim: false }); f.render_widget( log_paragraph, chunks[if app.fetch_complete { 1 } else { 2 }], ); - if let Some(prompt) = &app.prompt { draw_prompt(f, prompt); } + if enable_auto_scroll { + app.auto_scroll = true; + } while let Ok(event) = input_rx.try_recv() { - if app.prompt.is_some() { - if let Some(res) = handle_prompt_input(event, &mut app, &running) { - prompting.swap(res as u32, Ordering::SeqCst); - if res == PromptOption::Exit { - app.dump_logs_on_exit = Some(log_text.join("\n")) + 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) = get_active_log(&app); + if let Some(name) = name + && let Some(log) = log + { + app.dump_logs_on_exit = Some((name.to_owned(), log.join("\n"))); } - app.prompt = None; + running.store(false, Ordering::SeqCst); } + app.prompt = None; } else { - handle_main_event(&mut app, event); + handle_main_event(&mut app, &event); } } })?; @@ -819,7 +839,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result (Option, Option<&Vec>) { + 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 + }; + (active_name, log_text) +} + +fn handle_main_event(app: &mut TuiApp, event: &Event) { match event { Event::Key(key) => match key { Key::Char('1') => { @@ -904,27 +939,22 @@ fn handle_main_event(app: &mut TuiApp, event: Event) { } } -fn handle_prompt_input( - event: Event, - app: &mut TuiApp, - running: &Arc, -) -> Option { +fn handle_prompt_input<'a>( + event: &Event, + app: &'a mut TuiApp, +) -> Option<(&'a mut TuiApp, PromptOption)> { if let Some(prompt) = &mut app.prompt { match event { Event::Key(key) => match key { Key::Char('q') | Key::Ctrl('c') | Key::Esc => { // Treat as "Exit" - running.store(false, Ordering::SeqCst); - return Some(PromptOption::Exit); + return Some((app, PromptOption::Exit)); } Key::Left | Key::BackTab => prompt.prev(), Key::Right | Key::Char('\t') => prompt.next(), Key::Char('\n') => { let prompt = app.prompt.take().unwrap(); - if prompt.selected == PromptOption::Exit { - running.store(false, Ordering::SeqCst); - } - return Some(prompt.selected); + return Some((app, prompt.selected)); } _ => {} }, diff --git a/src/cook/package.rs b/src/cook/package.rs index a02ad9b1..b4fed138 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -36,6 +36,7 @@ pub fn package( } let package_file = target_dir.join("stage.pkgar"); + let package_meta = target_dir.join("stage.toml"); // Rebuild package if stage is newer //TODO: rebuild on recipe changes if package_file.is_file() { @@ -47,6 +48,7 @@ pub fn package( package_file.display() ); remove_all(&package_file)?; + remove_all(&package_meta)?; } } if !package_file.is_file() { @@ -56,7 +58,9 @@ pub fn package( stage_dir.to_str().unwrap(), ) .map_err(|err| format!("failed to create pkgar archive: {:?}", err))?; + } + if !package_meta.is_file() { package_toml(target_dir, name, recipe, auto_deps)?; } From d91f0eb8d520a581b8e87e7f7fb272db4d4e56d5 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 12:17:11 +0700 Subject: [PATCH 12/26] Fix package log --- src/bin/cook.rs | 2 +- src/bin/repo.rs | 1 + src/cook/package.rs | 19 ++++++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/bin/cook.rs b/src/bin/cook.rs index 82e05744..e70bf4bb 100644 --- a/src/bin/cook.rs +++ b/src/bin/cook.rs @@ -44,7 +44,7 @@ fn cook( ) .map_err(|err| format!("failed to build: {}", err))?; - package(&stage_dir, &target_dir, name, recipe, &auto_deps) + package(&stage_dir, &target_dir, name, recipe, &auto_deps, &None) .map_err(|err| format!("failed to package: {}", err))?; Ok(()) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 378118fb..c8f9c80b 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -314,6 +314,7 @@ fn handle_cook( &recipe.name, &recipe.recipe, &auto_deps, + logger, ) .map_err(|err| anyhow!("failed to package: {:?}", err))?; diff --git a/src/cook/package.rs b/src/cook/package.rs index b4fed138..3103c13f 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -7,12 +7,28 @@ use crate::{ recipe::{BuildKind, Recipe}, }; +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 fn package( stage_dir: &Path, target_dir: &Path, name: &PackageName, recipe: &Recipe, auto_deps: &BTreeSet, + logger: &Stdout, ) -> Result<(), String> { if recipe.build.kind == BuildKind::None { // metapackages don't have stage dir @@ -42,7 +58,8 @@ pub fn package( if package_file.is_file() { let stage_modified = modified_dir(stage_dir)?; if modified(&package_file)? < stage_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", stage_dir.display(), package_file.display() From 067274504d18247befefb5f2ac78eb986dda3289 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 12:23:14 +0700 Subject: [PATCH 13/26] Fix warn --- src/bin/repo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index c8f9c80b..9b9e0796 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -504,7 +504,7 @@ impl TuiApp { .recipes .iter() .filter(|(_, s)| *s == RecipeStatus::Fetched) - .map(|(r, _)| (r.clone())) + .map(|(r, _)| r.clone()) .collect(); self.done = self .recipes From 911e813025e15546b29ab259b0d9696cfa779f99 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 16:17:37 +0700 Subject: [PATCH 14/26] More context messages, add find cmd --- src/bin/repo.rs | 144 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 29 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 9b9e0796..085d6dee 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -42,6 +42,7 @@ const REPO_HELP_STR: &str = r#" unfetch delete recipe sources clean delete recipe artifacts push extract package into sysroot + find find path of recipe packages tree show tree of recipe packages common flags: @@ -49,13 +50,15 @@ const REPO_HELP_STR: &str = r#" --repo= the "repo" folder, default to $PWD/repo --sysroot= the "root" folder used for "push" command For Redox, defaults to "/", else default to $PWD/sysroot - - cook flags: --with-package-deps include package deps - --offline prefer to not use network - --nonstop keep running even a recipe build failed --all apply to all recipes in - -q, --quiet surpress build logs unless error + + cook env and their defaults: + CI= set to any value to disable TUI + COOKBOOK_OFFLINE=false prevent internet access if possible + COOKBOOK_NONSTOP=false pkeep running even a recipe build failed + COOKBOOK_VERBOSE=true print success/error on each recipe + COOKBOOK_MAKE_JOBS= override build jobs count from nproc "#; #[derive(Clone)] @@ -76,6 +79,13 @@ enum CliCommand { Clean, Push, Tree, + Find, +} + +impl CliCommand { + pub fn is_informational(&self) -> bool { + *self == CliCommand::Tree || *self == CliCommand::Find + } } impl FromStr for CliCommand { @@ -89,7 +99,8 @@ impl FromStr for CliCommand { "clean" => Ok(CliCommand::Clean), "push" => Ok(CliCommand::Push), "tree" => Ok(CliCommand::Tree), - _ => Err(anyhow!("Unknown command '{}'", s)), + "find" => Ok(CliCommand::Find), + _ => Err(anyhow!("Unknown command '{}'\n{}\n", s, REPO_HELP_STR)), } } } @@ -103,6 +114,7 @@ impl ToString for CliCommand { CliCommand::Clean => "clean".to_string(), CliCommand::Push => "push".to_string(), CliCommand::Tree => "tree".to_string(), + CliCommand::Find => "find".to_string(), } } } @@ -143,7 +155,6 @@ fn main_inner() -> anyhow::Result<()> { } let (config, command, recipe_names) = parse_args(args)?; - if command == CliCommand::Cook && config.cook.tui { if let Some((name, e)) = run_tui_cook(config, recipe_names)? { let _ = stderr().write(e.as_bytes()); @@ -170,28 +181,67 @@ fn main_inner() -> anyhow::Result<()> { } for recipe in &recipe_names { - match command { - CliCommand::Fetch => { - handle_fetch(recipe, &config, &None)?; + match repo_inner(&config, &command, recipe) { + Ok(_) => { + eprintln!( + "{}{}{} {} - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(46)), + command.to_string(), + recipe.name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); } - CliCommand::Cook => { - let source_dir = handle_fetch(recipe, &config, &None)?; - handle_cook(recipe, &config, source_dir, recipe.is_deps, &None)? + Err(e) => { + if config.cook.nonstop { + eprintln!("{:?}", e); + } + eprintln!( + "{}{}{} {} - failed {}{}", + style::Bold, + color::Fg(color::AnsiValue(196)), + command.to_string(), + recipe.name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); + if !config.cook.nonstop { + return Err(e); + } } - CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, - CliCommand::Clean => handle_clean(recipe, &config, false, true)?, - CliCommand::Push => handle_push(recipe, &config)?, - CliCommand::Tree => todo!("tree command is WIP"), } } println!( - "\nCommand '{}' completed for all specified recipes.", + "\nCommand '{}' completed for {} recipes.", command.to_string(), + recipe_names.len() ); Ok(()) } +fn repo_inner( + config: &CliConfig, + command: &CliCommand, + recipe: &CookRecipe, +) -> Result<(), anyhow::Error> { + Ok(match *command { + CliCommand::Fetch => { + handle_fetch(recipe, config, &None)?; + } + CliCommand::Cook => { + let source_dir = handle_fetch(recipe, config, &None)?; + handle_cook(recipe, config, source_dir, recipe.is_deps, &None)? + } + CliCommand::Unfetch => handle_clean(recipe, config, true, true)?, + CliCommand::Clean => handle_clean(recipe, config, false, true)?, + CliCommand::Push => handle_push(recipe, config)?, + CliCommand::Tree => todo!("tree command is WIP"), + CliCommand::Find => println!("{}", recipe.dir.display()), + }) +} + fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec)> { let mut config = CliConfig::new()?; let mut command: Option = None; @@ -240,11 +290,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec String { + match self { + JobType::Fetch => "Fetch", + JobType::Cook => "Cook", + } + .to_string() + } +} + struct TuiApp { recipes: Vec<(CookRecipe, RecipeStatus)>, fetch_queue: VecDeque, @@ -683,9 +751,9 @@ fn run_tui_cook( terminal.draw(|f| { let mut constraints = Vec::new(); if !app.fetch_complete { - constraints.push(Constraint::Length(30)); + constraints.push(Constraint::Length(20)); } - constraints.push(Constraint::Length(30)); + constraints.push(Constraint::Length(20)); constraints.push(Constraint::Min(20)); let chunks = Layout::default() .direction(Direction::Horizontal) @@ -744,9 +812,13 @@ fn run_tui_cook( let (active_name, log_text) = get_active_log(&app); let log_title = if let Some(active_name) = active_name { - format!("Build Log: {}", active_name.as_str()) + format!( + "{} Log: {}", + app.log_view_job.to_string(), + active_name.as_str() + ) } else { - "Build Log".to_string() + format!("{} Log", app.log_view_job.to_string()) }; let mut enable_auto_scroll = false; @@ -788,8 +860,22 @@ fn run_tui_cook( vec![Line::from("No logs yet")] }; + let instruct = format!( + "Keys: [c] Stop [PageUp/Down] Scroll {}", + match (&app.log_view_job, app.fetch_complete) { + (JobType::Fetch, _) => "[2] View Cook Log", + (JobType::Cook, false) => "[1] View Fetch Log", + (JobType::Cook, true) => "", + } + ); + let log_paragraph = Paragraph::new(log_lines) - .block(Block::default().title(log_title).borders(Borders::ALL)) + .block( + Block::default() + .title(log_title) + .title_bottom(instruct) + .borders(Borders::ALL), + ) .wrap(Wrap { trim: false }); f.render_widget( From 1e37be1d4f714f1de8bfd7305bbe66c6665befaa Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 17:48:03 +0700 Subject: [PATCH 15/26] Add category, spinner, and always verbose --- src/bin/repo.rs | 129 ++++++++++++++++++++++++++++++++++-------------- src/config.rs | 8 +-- 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 085d6dee..29d56fcb 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -5,7 +5,7 @@ use std::process::Command; use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, mpsc}; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::{cmp, env, fs}; use std::{process, thread}; @@ -52,6 +52,7 @@ const REPO_HELP_STR: &str = r#" For Redox, defaults to "/", else default to $PWD/sysroot --with-package-deps include package deps --all apply to all recipes in + --category= apply to all recipes in / cook env and their defaults: CI= set to any value to disable TUI @@ -66,6 +67,7 @@ struct CliConfig { cookbook_dir: PathBuf, repo_dir: PathBuf, sysroot_dir: PathBuf, + category: Option, with_package_deps: bool, all: bool, cook: CookConfig, @@ -126,6 +128,7 @@ 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"), + category: None, sysroot_dir: if cfg!(target_os = "redox") { PathBuf::from("/") } else { @@ -180,21 +183,24 @@ fn main_inner() -> anyhow::Result<()> { return Ok(()); } + let verbose = config.cook.verbose; for recipe in &recipe_names { match repo_inner(&config, &command, recipe) { Ok(_) => { - eprintln!( - "{}{}{} {} - successful{}{}", - style::Bold, - color::Fg(color::AnsiValue(46)), - command.to_string(), - recipe.name.as_str(), - color::Fg(color::Reset), - style::Reset, - ); + if verbose { + eprintln!( + "{}{}{} {} - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(46)), + command.to_string(), + recipe.name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); + } } Err(e) => { - if config.cook.nonstop { + if config.cook.nonstop && verbose { eprintln!("{:?}", e); } eprintln!( @@ -213,11 +219,13 @@ fn main_inner() -> anyhow::Result<()> { } } - println!( - "\nCommand '{}' completed for {} recipes.", - command.to_string(), - recipe_names.len() - ); + if verbose { + println!( + "\nCommand '{}' completed for {} recipes.", + command.to_string(), + recipe_names.len() + ); + } Ok(()) } @@ -253,6 +261,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec config.cookbook_dir = PathBuf::from(value), "--repo" => config.repo_dir = PathBuf::from(value), "--sysroot" => config.sysroot_dir = PathBuf::from(value), + "--category" => config.category = Some(PathBuf::from(value)), _ => { eprintln!("Error: Unknown flag with value: {}", arg); process::exit(1); @@ -284,25 +293,38 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec, PackageError>>()? + match &config.category { + None => pkg::recipes::list(""), + Some(prefix) => pkg::recipes::list("") + .into_iter() + .filter(|p| p.starts_with(prefix)) + .collect(), + } + .iter() + .map(|f| CookRecipe::from_path(f, !config.all)) + .collect::, PackageError>>()? } else { if recipe_names.is_empty() { bail!("Error: No recipe names provided and --all flag was not used."); @@ -592,6 +614,7 @@ fn run_tui_cook( let running = Arc::new(AtomicBool::new(true)); let prompting = Arc::new(AtomicU32::new(0)); + const TICK_RATE: Duration = Duration::from_millis(100); // ---- Cooker Thread ---- let cooker_config = config.clone(); @@ -747,13 +770,20 @@ fn run_tui_cook( let mut app = TuiApp::new(recipes); + let spinner = ['-', '\\', '|', '/']; + let mut spinner_i = 0; + while running.load(Ordering::SeqCst) { + let frame_start = Instant::now(); terminal.draw(|f| { + spinner_i = (spinner_i + 1) % spinner.len(); + let spin = spinner[spinner_i]; + let mut constraints = Vec::new(); if !app.fetch_complete { - constraints.push(Constraint::Length(20)); + constraints.push(Constraint::Length(22)); } - constraints.push(Constraint::Length(20)); + constraints.push(Constraint::Length(22)); constraints.push(Constraint::Min(20)); let chunks = Layout::default() .direction(Direction::Horizontal) @@ -771,7 +801,13 @@ fn run_tui_cook( } else { Style::default() }; - ListItem::new(r.name.as_str()).style(style) + let icon = match s { + RecipeStatus::Pending => ' ', + RecipeStatus::Fetching => spin, + _ => '?', + }; + + ListItem::new(format!("{icon} {}", r.name)).style(style) }) .collect(); let fetch_list = List::new(fetch_items).block( @@ -799,7 +835,14 @@ fn run_tui_cook( RecipeStatus::Failed(_) => Style::default().fg(Color::Red), _ => Style::default(), }; - ListItem::new(r.name.as_str()).style(style) + let icon = match s { + RecipeStatus::Fetched => ' ', + RecipeStatus::Cooking => spin, + RecipeStatus::Done => ' ', + RecipeStatus::Failed(_) => 'X', + _ => '?', + }; + ListItem::new(format!("{icon} {}", r.name)).style(style) }) .collect(); let cook_list = List::new(cook_items).block( @@ -813,12 +856,12 @@ fn run_tui_cook( let log_title = if let Some(active_name) = active_name { format!( - "{} Log: {}", + " {} Log: {} ", app.log_view_job.to_string(), active_name.as_str() ) } else { - format!("{} Log", app.log_view_job.to_string()) + format!(" {} Log ", app.log_view_job.to_string()) }; let mut enable_auto_scroll = false; @@ -861,10 +904,14 @@ fn run_tui_cook( }; let instruct = format!( - "Keys: [c] Stop [PageUp/Down] Scroll {}", + " Keys: [c] Stop [PageUp/Down] Scroll{}{} ", + match app.auto_scroll { + true => "", + false => " [End] Follow log trails", + }, match (&app.log_view_job, app.fetch_complete) { - (JobType::Fetch, _) => "[2] View Cook Log", - (JobType::Cook, false) => "[1] View Fetch Log", + (JobType::Fetch, _) => " [2] View Cook Log", + (JobType::Cook, false) => " [1] View Fetch Log", (JobType::Cook, true) => "", } ); @@ -915,6 +962,10 @@ fn run_tui_cook( if app.cook_complete { running.swap(false, Ordering::SeqCst); } + + if let Some(sleep_duration) = TICK_RATE.checked_sub(frame_start.elapsed()) { + thread::sleep(sleep_duration); + } } drop(mstdout); @@ -1054,7 +1105,11 @@ fn handle_prompt_input<'a>( fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { let title = format!(" FAILURE in {} ", prompt.recipe.name); let mut error_text = prompt.error.clone(); - if error_text.len() > 100 { + if error_text.len() > 200 { + error_text = error_text[0..100].to_string() + + ".." + + &error_text[(error_text.len() - 100)..(error_text.len() - 1)]; + } else if error_text.len() > 100 { error_text = error_text[0..100].to_string() + ".."; } @@ -1079,11 +1134,11 @@ fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { Line::from(error_text).style(Style::default().fg(Color::Yellow)), Line::from(""), Line::from(vec![ - Span::styled(" [Retry] ", retry_style), - Span::raw(" "), Span::styled(" [Skip] ", skip_style), Span::raw(" "), Span::styled(" [Exit] ", exit_style), + Span::raw(" "), + Span::styled(" [Retry] ", retry_style), ]), ]; diff --git a/src/config.rs b/src/config.rs index 313acbc0..27e42dd7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,8 +14,7 @@ pub struct CookConfigOpt { pub tui: Option, /// whether to ignore build errors pub nonstop: Option, - /// whether to not capture build output, - /// default is true if "tui" is false. + /// whether to print success recipes info and warnings /// build failure still be printed anyway pub verbose: Option, } @@ -80,10 +79,7 @@ pub fn init_config() { config.cook_opt.offline = Some(extract_env("COOKBOOK_OFFLINE", false)); } if config.cook_opt.verbose.is_none() { - config.cook_opt.verbose = Some(extract_env( - "COOKBOOK_VERBOSE", - !config.cook_opt.tui.unwrap(), - )); + config.cook_opt.verbose = Some(extract_env("COOKBOOK_VERBOSE", true)); } if config.cook_opt.nonstop.is_none() { config.cook_opt.nonstop = Some(extract_env("COOKBOOK_NONSTOP", false)); From 5621fe799f07adaa6d3bc1e3362588141b7aaf29 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 17:53:13 +0700 Subject: [PATCH 16/26] Update scripts for compat --- clean.sh | 2 +- config.sh | 5 ++++- fetch.sh | 3 ++- repo.sh | 8 ++------ unfetch.sh | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/clean.sh b/clean.sh index 211fcb7c..efde8b91 100755 --- a/clean.sh +++ b/clean.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh if [ $# = 0 ] then diff --git a/config.sh b/config.sh index f60c503e..8e980198 100755 --- a/config.sh +++ b/config.sh @@ -17,8 +17,11 @@ if [ x"${HOST}" == x"riscv64gc-unknown-redox" ] ; then HOST="riscv64-unknown-redox" fi +# Cookbook requires correct CWD to work +cd `dirname "$0"` + # Automatic variables -ROOT="$(cd `dirname "$0"` && pwd)" +ROOT=`pwd` export AR="${HOST}-gcc-ar" export AS="${HOST}-as" diff --git a/fetch.sh b/fetch.sh index 16e503fd..cd9acc9c 100755 --- a/fetch.sh +++ b/fetch.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh +# Intentionally empty to allow fetch and cook running in parallel diff --git a/repo.sh b/repo.sh index 3ef74d17..acfffb47 100755 --- a/repo.sh +++ b/repo.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash set -e -shopt -s nullglob -source config.sh +source `dirname "$0"`/config.sh APPSTREAM="0" COOK_OPT="" @@ -17,7 +16,7 @@ do COOK_OPT+=" --with-package-deps" elif [ "$arg" == "--nonstop" ] then - COOK_OPT+=" --nonstop" + export COOKBOOK_OFFLINE=true else recipes+=" $arg" fi @@ -25,7 +24,4 @@ done repo cook $COOK_OPT $recipes -repo="$ROOT/repo/$TARGET" -mkdir -p "$repo" - repo_builder "$repo" $recipes diff --git a/unfetch.sh b/unfetch.sh index 05934936..4b7b6262 100755 --- a/unfetch.sh +++ b/unfetch.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh if [ $# = 0 ] then From 4fb4727f60b374616777fbf517641012f4910e23 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 19:23:03 +0700 Subject: [PATCH 17/26] Autoscroll cook queue --- src/bin/repo.rs | 67 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 29d56fcb..9e4f2865 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -24,7 +24,7 @@ use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; use termion::event::{Event, Key, MouseEvent}; use termion::input::TermRead; use termion::raw::IntoRawMode; @@ -489,6 +489,8 @@ struct TuiApp { auto_scroll: bool, fetch_scroll: usize, cook_scroll: usize, + cook_auto_scroll: bool, + cook_list_state: ListState, fetch_complete: bool, cook_complete: bool, fetch_panel_rect: Option, @@ -517,6 +519,8 @@ impl TuiApp { log_view_job: JobType::Fetch, fetch_scroll: 0, cook_scroll: 0, + cook_auto_scroll: true, + cook_list_state: ListState::default(), fetch_complete: false, cook_complete: false, fetch_panel_rect: None, @@ -789,6 +793,7 @@ fn run_tui_cook( .direction(Direction::Horizontal) .constraints(constraints) .split(f.area()); + let panel_height = chunks[0].height.saturating_sub(2) as usize; // Left Pane let fetch_items: Vec = app @@ -845,12 +850,46 @@ fn run_tui_cook( ListItem::new(format!("{icon} {}", r.name)).style(style) }) .collect(); + let total_items = cook_items.len(); + if app.cook_auto_scroll { + let cooking_index = app + .recipes + .iter() + .filter(|(_, s)| { + *s == RecipeStatus::Fetched + || *s == RecipeStatus::Cooking + || *s == RecipeStatus::Done + || matches!(s, RecipeStatus::Failed(_)) + }) + .position(|(_r, s)| *s == RecipeStatus::Cooking); + + if let Some(index) = cooking_index { + app.cook_list_state.select(Some(index)); + let index_u16 = index; + let center_offset = panel_height / 2; + let new_offset = index_u16.saturating_sub(center_offset) as usize; + + *app.cook_list_state.offset_mut() = new_offset; + } + } else { + app.cook_list_state.select(None); + if total_items > 0 { + let max_offset = total_items.saturating_sub(panel_height as usize); + if *app.cook_list_state.offset_mut() > max_offset { + *app.cook_list_state.offset_mut() = max_offset; + } + } else { + *app.cook_list_state.offset_mut() = 0; + } + } + let cook_items: Vec = cook_items[app.cook_scroll..].into(); + let cook_chunk = chunks[if app.fetch_complete { 0 } else { 1 }]; let cook_list = List::new(cook_items).block( Block::default() .title("Cook Queue [2]") .borders(Borders::ALL), ); - f.render_widget(cook_list, chunks[if app.fetch_complete { 0 } else { 1 }]); + f.render_stateful_widget(cook_list, cook_chunk, &mut app.cook_list_state); let (active_name, log_text) = get_active_log(&app); @@ -865,26 +904,26 @@ fn run_tui_cook( }; let mut enable_auto_scroll = false; + let mut intended_scroll_pos = 0usize; let log_lines: Vec = if let Some(log_text) = log_text && log_text.len() > 0 { - let log_pane_height = chunks[if app.fetch_complete { 1 } else { 2 }] - .height - .saturating_sub(2) as usize; let total_log_lines = log_text.len() as usize; let start = if app.auto_scroll { - if total_log_lines > log_pane_height { - total_log_lines - log_pane_height + if total_log_lines > panel_height { + intended_scroll_pos = total_log_lines - panel_height; + total_log_lines - panel_height } else { 0 } } else { - if total_log_lines > log_pane_height { - if app.log_scroll >= total_log_lines - log_pane_height { + if total_log_lines > panel_height { + if app.log_scroll >= total_log_lines - panel_height { enable_auto_scroll = true; - total_log_lines - log_pane_height + intended_scroll_pos = total_log_lines - panel_height; + total_log_lines - panel_height } else { app.log_scroll } @@ -893,7 +932,7 @@ fn run_tui_cook( } }; - let end = cmp::min(log_pane_height + start, total_log_lines - 1); + let end = cmp::min(panel_height + start, total_log_lines - 1); log_text[start..end] .iter() @@ -935,6 +974,9 @@ fn run_tui_cook( if enable_auto_scroll { app.auto_scroll = true; } + if intended_scroll_pos > 0 { + app.log_scroll = intended_scroll_pos; + } while let Ok(event) = input_rx.try_recv() { if let Some((app, res)) = handle_prompt_input(&event, &mut app) { @@ -1037,6 +1079,7 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) { _ => {} }, + //FIXME: This does nothing, it seems ratatui handles this itself magically Event::Mouse(mouse_event) => { match mouse_event { MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { @@ -1050,6 +1093,7 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) { app.fetch_scroll = app.fetch_scroll.saturating_sub(1); } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { app.cook_scroll = app.cook_scroll.saturating_sub(1); + app.cook_auto_scroll = false; } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { app.auto_scroll = false; app.log_scroll = app.log_scroll.saturating_sub(1); @@ -1065,6 +1109,7 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) { app.fetch_scroll = app.fetch_scroll.saturating_add(1); } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { app.cook_scroll = app.cook_scroll.saturating_add(1); + app.cook_auto_scroll = false; } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { app.auto_scroll = false; app.log_scroll = app.log_scroll.saturating_add(1); From 66d7a520e1537130629a10a0ea9fe7f647d73725 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 19:34:51 +0700 Subject: [PATCH 18/26] Fix repo script --- repo.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/repo.sh b/repo.sh index acfffb47..0a95ac89 100755 --- a/repo.sh +++ b/repo.sh @@ -15,6 +15,9 @@ do then COOK_OPT+=" --with-package-deps" elif [ "$arg" == "--nonstop" ] + then + COOK_OPT+=" --nonstop" + elif [ "$arg" == "--offline" ] then export COOKBOOK_OFFLINE=true else @@ -24,4 +27,4 @@ done repo cook $COOK_OPT $recipes -repo_builder "$repo" $recipes +repo_builder "$ROOT/repo/$TARGET" $recipes From a943426bde65ef8fc00e86976a964ead454f1bac Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sun, 26 Oct 2025 22:06:59 +0700 Subject: [PATCH 19/26] Try use pty --- Cargo.lock | 99 +++++++++- Cargo.toml | 3 + src/bin/repo.rs | 31 ++-- src/cook.rs | 1 + src/cook/cook_build.rs | 9 +- src/cook/fetch.rs | 9 +- src/cook/fs.rs | 39 ++-- src/cook/package.rs | 22 +-- src/cook/pty.rs | 403 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 546 insertions(+), 70 deletions(-) create mode 100644 src/cook/pty.rs diff --git a/Cargo.lock b/Cargo.lock index 58737f92..907d9d9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,12 @@ 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" @@ -753,6 +759,12 @@ 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" @@ -854,6 +866,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "flate2" version = "1.1.2" @@ -1537,6 +1560,18 @@ 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 = "num-conv" version = "0.1.0" @@ -1748,6 +1783,27 @@ 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" @@ -1797,7 +1853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -1837,7 +1893,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2", @@ -2007,12 +2063,15 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3 1.5.3", + "filedescriptor", "ignore", + "libc", "object", "pbr", "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", + "portable-pty", "ratatui", "redox-pkg", "redoxer", @@ -2434,6 +2493,17 @@ 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" @@ -2445,6 +2515,22 @@ 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" @@ -3445,6 +3531,15 @@ 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 8b28c1c1..ae5e18c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,14 @@ doctest = false [dependencies] anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar +libc = "0.2" ignore = "0.4" object = { version = "0.36", features = ["build_core"] } 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" redoxer = "0.2" regex = "1.11" @@ -36,6 +38,7 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" +filedescriptor = "0.8.3" [dependencies.ratatui] version = "0.29.0" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 9e4f2865..6e643f4a 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,5 +1,5 @@ use std::collections::{HashMap, VecDeque}; -use std::io::{BufRead, BufReader, PipeReader, Write, stderr, stdin, stdout}; +use std::io::{BufRead, BufReader, Read, Write, stderr, stdin, stdout}; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -14,8 +14,9 @@ use cookbook::WALK_DEPTH; use cookbook::config::{CookConfig, get_config, init_config}; use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; -use cookbook::cook::fs::{Stdout, create_target_dir}; +use cookbook::cook::fs::create_target_dir; use cookbook::cook::package::package; +use cookbook::cook::pty::{setup_pty, PtyOut, UnixSlavePty}; use cookbook::recipe::CookRecipe; use pkg::PackageName; use pkg::package::PackageError; @@ -355,7 +356,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { @@ -372,7 +373,7 @@ fn handle_cook( config: &CliConfig, source_dir: PathBuf, is_deps: bool, - logger: &Stdout, + logger: &PtyOut, ) -> anyhow::Result<()> { let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; @@ -1212,13 +1213,15 @@ fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { f.render_widget(paragraph, popup_area); } -fn spawn_log_reader( - mut pipe_reader: PipeReader, +fn spawn_log_reader( + mut reader: R, package_name: PackageName, status_tx: mpsc::Sender, -) { +) where + R: Read + Send + 'static, +{ thread::spawn(move || { - let reader = BufReader::new(&mut pipe_reader); + let reader = BufReader::new(&mut reader); for line in reader.lines() { let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); if status_tx @@ -1235,12 +1238,12 @@ fn spawn_log_reader( fn setup_logger( status_tx: &mpsc::Sender, name: &PackageName, -) -> (std::io::PipeWriter, std::io::PipeWriter) { - let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); - let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); - spawn_log_reader(stdout_reader, name.clone(), status_tx.clone()); - spawn_log_reader(stderr_reader, name.clone(), status_tx.clone()); - (stdout_writer, stderr_writer) +) -> (UnixSlavePty, std::io::PipeWriter) { + let (pty_reader, log_reader, pipes) = setup_pty(); + + spawn_log_reader(pty_reader, name.clone(), status_tx.clone()); + spawn_log_reader(log_reader, name.clone(), status_tx.clone()); + pipes } #[derive(PartialEq, Clone, Copy)] diff --git a/src/cook.rs b/src/cook.rs index 4ffa8a7b..14c752f8 100644 --- a/src/cook.rs +++ b/src/cook.rs @@ -3,4 +3,5 @@ pub mod cook_build; pub mod fetch; pub mod fs; pub mod package; +pub mod pty; pub mod script; diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index a9c92164..e379fce6 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -3,6 +3,7 @@ use pkg::{Package, PackageName}; use redoxer::target; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::recipe::AutoDeps; use crate::recipe::BuildKind; @@ -39,7 +40,7 @@ macro_rules! log_warn { fn auto_deps( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, - logger: &Stdout, + logger: &PtyOut, ) -> BTreeSet { let mut paths = BTreeSet::new(); let mut visited = BTreeSet::new(); @@ -167,7 +168,7 @@ pub fn build( recipe: &Recipe, offline_mode: bool, check_source: bool, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); @@ -379,7 +380,7 @@ fn build_auto_deps( target_dir: &Path, stage_dir: &PathBuf, dep_pkgars: BTreeSet<(PackageName, PathBuf)>, - logger: &Stdout, + logger: &PtyOut, ) -> Result, String> { let auto_deps_path = target_dir.join("auto_deps.toml"); if auto_deps_path.is_file() && modified(&auto_deps_path)? < modified(stage_dir)? { @@ -412,7 +413,7 @@ pub fn build_remote( target_dir: &Path, name: &PackageName, offline_mode: bool, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { // download straight from remote source then declare pkg dependencies as autodeps dependency let stage_dir = target_dir.join("stage"); diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 6295cae7..7f569fb5 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -1,5 +1,6 @@ use crate::config::translate_mirror; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::is_redox; use crate::recipe::BuildKind; @@ -43,7 +44,7 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { @@ -103,7 +104,7 @@ pub fn fetch_offline( Ok(source_dir) } -pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -374,7 +375,7 @@ pub(crate) fn fetch_resolve_canon( pub(crate) fn fetch_extract_tar( source_tar: PathBuf, source_dir_tmp: &PathBuf, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { let mut command = Command::new("tar"); if is_redox() { @@ -420,7 +421,7 @@ pub(crate) fn fetch_apply_patches( patches: &Vec, script: &Option, source_dir_tmp: &PathBuf, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { for patch_name in patches { let patch_file = recipe_dir.join(patch_name); diff --git a/src/cook/fs.rs b/src/cook/fs.rs index 6a5e8b88..db1a0fa2 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -8,7 +8,10 @@ use std::{ }; use walkdir::{DirEntry, WalkDir}; -use crate::config::translate_mirror; +use crate::{ + config::translate_mirror, + cook::pty::{PtyOut, spawn_to_pipe}, +}; //TODO: pub(crate) for all of these functions @@ -146,27 +149,10 @@ pub fn rename(src: &Path, dst: &Path) -> Result<(), String> { }) } -pub type Stdout<'a> = Option<(&'a mut PipeWriter, &'a mut PipeWriter)>; - -fn pipe_to_cmd(command: &mut Command, stdout_pipe: &Stdout) -> Result<(), String> { - Ok(if let Some((stdout, stderr)) = stdout_pipe { - command.stdout::( - stdout - .try_clone() - .map_err(|e| format!("unable to clone stdout fd: {:?}", e))?, - ); - command.stderr( - stderr - .try_clone() - .map_err(|e| format!("unable to clone stderr fd: {:?}", e))?, - ); - }) -} - -pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Result<(), String> { - pipe_to_cmd(&mut command, stdout_pipe)?; - let status = command - .status() +pub fn run_command(mut command: process::Command, stdout_pipe: &PtyOut) -> Result<(), String> { + let status = spawn_to_pipe(&mut command, stdout_pipe) + .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))? + .wait() .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))?; if !status.success() { @@ -182,13 +168,10 @@ pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Resul pub fn run_command_stdin( mut command: process::Command, stdin_data: &[u8], - stdout_pipe: &Stdout, + stdout_pipe: &PtyOut, ) -> Result<(), String> { command.stdin(Stdio::piped()); - pipe_to_cmd(&mut command, stdout_pipe)?; - - let mut child = command - .spawn() + let mut child = spawn_to_pipe(&mut command, stdout_pipe) .map_err(|err| format!("failed to spawn {:?}: {}\n{:#?}", command, err, err))?; if let Some(ref mut stdin) = child.stdin { @@ -240,7 +223,7 @@ pub fn offline_check_exists(path: &PathBuf) -> Result<(), String> { Ok(()) } -pub fn download_wget(url: &str, dest: &PathBuf, logger: &Stdout) -> Result<(), String> { +pub fn download_wget(url: &str, dest: &PathBuf, logger: &PtyOut) -> Result<(), String> { if !dest.is_file() { let dest_tmp = PathBuf::from(format!("{}.tmp", dest.display())); let mut command = Command::new("wget"); diff --git a/src/cook/package.rs b/src/cook/package.rs index 3103c13f..fa97817f 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -3,32 +3,18 @@ use std::{collections::BTreeSet, env, path::Path}; use pkg::{Package, PackageName}; use crate::{ - cook::fs::*, + cook::{fs::*, pty::PtyOut}, + log_to_pty, recipe::{BuildKind, Recipe}, }; -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 fn package( stage_dir: &Path, target_dir: &Path, name: &PackageName, recipe: &Recipe, auto_deps: &BTreeSet, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { if recipe.build.kind == BuildKind::None { // metapackages don't have stage dir @@ -58,7 +44,7 @@ pub fn package( if package_file.is_file() { let stage_modified = modified_dir(stage_dir)?; if modified(&package_file)? < stage_modified { - log_warn!( + log_to_pty!( logger, "DEBUG: '{}' newer than '{}'", stage_dir.display(), diff --git a/src/cook/pty.rs b/src/cook/pty.rs new file mode 100644 index 00000000..42876e9c --- /dev/null +++ b/src/cook/pty.rs @@ -0,0 +1,403 @@ +use anyhow::{Error, bail}; +use filedescriptor::FileDescriptor; +use libc::{self, winsize}; +use portable_pty::PtySize; +use std::cell::RefCell; +use std::ffi::OsStr; +use std::io::{Read, Write}; +use std::os::fd::FromRawFd; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::AsRawFd; +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::process::Child; +use std::{io, mem, ptr}; +use std::{ + io::{PipeReader, PipeWriter}, + process::Command, +}; + +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( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + +pub type PtyOut<'a> = Option<(&'a mut UnixSlavePty, &'a mut PipeWriter)>; + +pub fn setup_pty() -> ( + Box, + PipeReader, + (UnixSlavePty, std::io::PipeWriter), +) { + let pty_system = UnixPtySystem::default(); + let pair = pty_system + .openpty(PtySize { + rows: 24, // Standard terminal size + cols: 80, // Standard terminal size + ..Default::default() + }) + .expect("Unable to open pty"); + + // TODO: There's no way to handle stdin + let pty_reader = pair + .master + .try_clone_reader() + .expect("Unable to clone pty reader"); + + let (log_reader, log_writer) = std::io::pipe().expect("Failed to create log pipe"); + let pipes = (pair.slave, log_writer); + (pty_reader, log_reader, pipes) +} + +pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result { + match stdout_pipe { + Some(stdout) => stdout.0.spawn_command(command.into()), + None => Ok(command.spawn()?), + } +} + +// +// based on portable-pty crate +// copied here since it isn't flexible enough +// + +#[derive(Default)] +pub struct UnixPtySystem {} + +fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { + let mut master: RawFd = -1; + let mut slave: RawFd = -1; + + let mut size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + let result = unsafe { + // BSDish systems may require mut pointers to some args + #[allow(clippy::unnecessary_mut_passed)] + libc::openpty( + &mut master, + &mut slave, + ptr::null_mut(), + ptr::null_mut(), + &mut size, + ) + }; + + if result != 0 { + bail!("failed to openpty: {:?}", io::Error::last_os_error()); + } + + let tty_name = tty_name(slave); + + let master = UnixMasterPty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }), + took_writer: RefCell::new(false), + tty_name, + }; + let slave = UnixSlavePty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }), + }; + + // Ensure that these descriptors will get closed when we execute + // the child process. This is done after constructing the Pty + // instances so that we ensure that the Ptys get drop()'d if + // the cloexec() functions fail (unlikely!). + cloexec(master.fd.as_raw_fd())?; + cloexec(slave.fd.as_raw_fd())?; + + Ok((master, slave)) +} + +pub struct PtyPair { + // slave is listed first so that it is dropped first. + // The drop order is stable and specified by rust rfc 1857 + pub slave: UnixSlavePty, + pub master: UnixMasterPty, +} + +impl UnixPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result { + let (master, slave) = openpty(size)?; + Ok(PtyPair { + master: master, + slave: slave, + }) + } +} + +struct PtyFd(pub FileDescriptor); +impl std::ops::Deref for PtyFd { + type Target = FileDescriptor; + fn deref(&self) -> &FileDescriptor { + &self.0 + } +} +impl std::ops::DerefMut for PtyFd { + fn deref_mut(&mut self) -> &mut FileDescriptor { + &mut self.0 + } +} + +impl Read for PtyFd { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self.0.read(buf) { + Err(ref e) if e.raw_os_error() == Some(libc::EIO) => { + // EIO indicates that the slave pty has been closed. + // Treat this as EOF so that std::io::Read::read_to_string + // and similar functions gracefully terminate when they + // encounter this condition + Ok(0) + } + x => x, + } + } +} + +fn tty_name(fd: RawFd) -> Option { + let mut buf = vec![0 as std::ffi::c_char; 128]; + + loop { + let res = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr(), buf.len()) }; + + if res == libc::ERANGE { + if buf.len() > 64 * 1024 { + // on macOS, if the buf is "too big", ttyname_r can + // return ERANGE, even though that is supposed to + // indicate buf is "too small". + return None; + } + buf.resize(buf.len() * 2, 0 as std::ffi::c_char); + continue; + } + + return if res == 0 { + let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }; + let osstr = OsStr::from_bytes(cstr.to_bytes()); + Some(PathBuf::from(osstr)) + } else { + None + }; + } +} + +impl PtyFd { + fn resize(&self, size: PtySize) -> Result<(), Error> { + let ws_size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCSWINSZ as _, + &ws_size as *const _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCSWINSZ): {:?}", + io::Error::last_os_error() + ); + } + + Ok(()) + } + + fn get_size(&self) -> Result { + let mut size: winsize = unsafe { mem::zeroed() }; + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCGWINSZ as _, + &mut size as *mut _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCGWINSZ): {:?}", + io::Error::last_os_error() + ); + } + Ok(PtySize { + rows: size.ws_row, + cols: size.ws_col, + pixel_width: size.ws_xpixel, + pixel_height: size.ws_ypixel, + }) + } + + fn spawn_command(&self, cmd: &mut Command) -> anyhow::Result { + unsafe { + cmd + // .stdin(self.as_stdio()?) + .stdout(self.as_stdio()?) + .stderr(self.as_stdio()?) + .pre_exec(move || { + // Clean up a few things before we exec the program + // Clear out any potentially problematic signal + // dispositions that we might have inherited + for signo in &[ + libc::SIGCHLD, + libc::SIGHUP, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGTERM, + libc::SIGALRM, + ] { + libc::signal(*signo, libc::SIG_DFL); + } + + let empty_set: libc::sigset_t = std::mem::zeroed(); + libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut()); + + // Establish ourselves as a session leader. + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + }) + }; + + let mut child = cmd.spawn()?; + + // Ensure that we close out the slave fds that Child retains; + // they are not what we need (we need the master side to reference + // them) and won't work in the usual way anyway. + // In practice these are None, but it seems best to be move them + // out in case the behavior of Command changes in the future. + child.stdin.take(); + child.stdout.take(); + child.stderr.take(); + + Ok(child) + } +} + +/// Represents the master end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixMasterPty { + fd: PtyFd, + took_writer: RefCell, + tty_name: Option, +} + +/// Represents the slave end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixSlavePty { + fd: PtyFd, +} + +/// Helper function to set the close-on-exec flag for a raw descriptor +fn cloexec(fd: RawFd) -> Result<(), Error> { + let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; + if flags == -1 { + bail!( + "fcntl to read flags failed: {:?}", + io::Error::last_os_error() + ); + } + let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) }; + if result == -1 { + bail!( + "fcntl to set CLOEXEC failed: {:?}", + io::Error::last_os_error() + ); + } + Ok(()) +} + +impl UnixSlavePty { + fn spawn_command(&self, builder: &mut Command) -> Result { + Ok(self.fd.spawn_command(builder)?) + } +} + +impl UnixMasterPty { + fn resize(&self, size: PtySize) -> Result<(), Error> { + self.fd.resize(size) + } + + fn get_size(&self) -> Result { + self.fd.get_size() + } + + fn try_clone_reader(&self) -> Result, Error> { + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(fd)) + } + + fn take_writer(&self) -> Result, Error> { + if *self.took_writer.borrow() { + anyhow::bail!("cannot take writer more than once"); + } + *self.took_writer.borrow_mut() = true; + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(UnixMasterWriter { fd })) + } + + fn as_raw_fd(&self) -> Option { + Some(self.fd.0.as_raw_fd()) + } + + fn tty_name(&self) -> Option { + self.tty_name.clone() + } + + fn process_group_leader(&self) -> Option { + match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } { + pid if pid > 0 => Some(pid), + _ => None, + } + } +} + +/// Represents the master end of a pty. +/// EOT will be sent, and then the file descriptor will be closed when +/// the Pty is dropped. +struct UnixMasterWriter { + fd: PtyFd, +} + +impl Drop for UnixMasterWriter { + fn drop(&mut self) { + let mut t: libc::termios = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + if unsafe { libc::tcgetattr(self.fd.0.as_raw_fd(), &mut t) } == 0 { + // EOF is only interpreted after a newline, so if it is set, + // we send a newline followed by EOF. + let eot = t.c_cc[libc::VEOF]; + if eot != 0 { + let _ = self.fd.0.write_all(&[b'\n', eot]); + } + } + } +} + +impl Write for UnixMasterWriter { + fn write(&mut self, buf: &[u8]) -> Result { + self.fd.write(buf) + } + fn flush(&mut self) -> Result<(), io::Error> { + self.fd.flush() + } +} From 1ecf2311e40a753fa95811eb1b065e18a2751dbc Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sun, 26 Oct 2025 22:20:30 +0700 Subject: [PATCH 20/26] Add ansi escape --- Cargo.lock | 36 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/bin/repo.rs | 48 +++++++++++++++++++++++++++++------------------- src/cook/pty.rs | 2 +- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 907d9d9e..74995894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-to-tui" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" +dependencies = [ + "nom", + "ratatui", + "simdutf8", + "smallvec", + "thiserror 1.0.69", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -1540,6 +1553,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1572,6 +1591,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2061,6 +2090,7 @@ dependencies = [ name = "redox_cookbook" version = "0.1.0" dependencies = [ + "ansi-to-tui", "anyhow", "blake3 1.5.3", "filedescriptor", @@ -2537,6 +2567,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.10" diff --git a/Cargo.toml b/Cargo.toml index ae5e18c9..e21950d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ termion = "4" toml = "0.8" walkdir = "2.3.1" filedescriptor = "0.8.3" +ansi-to-tui = "7.0.0" [dependencies.ratatui] version = "0.29.0" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 6e643f4a..6097b575 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,3 +1,21 @@ +use ansi_to_tui::IntoText; +use anyhow::{Context, anyhow, bail}; +use cookbook::WALK_DEPTH; +use cookbook::config::{CookConfig, get_config, init_config}; +use cookbook::cook::cook_build::build; +use cookbook::cook::fetch::{fetch, fetch_offline}; +use cookbook::cook::fs::create_target_dir; +use cookbook::cook::package::package; +use cookbook::cook::pty::{PtyOut, UnixSlavePty, setup_pty}; +use cookbook::recipe::CookRecipe; +use pkg::PackageName; +use pkg::package::PackageError; +use ratatui::Terminal; +use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; +use ratatui::prelude::TermionBackend; +use ratatui::style::{Color, Style}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; use std::collections::{HashMap, VecDeque}; use std::io::{BufRead, BufReader, Read, Write, stderr, stdin, stdout}; use std::path::PathBuf; @@ -8,24 +26,6 @@ use std::sync::{Arc, mpsc}; use std::time::{Duration, Instant}; use std::{cmp, env, fs}; use std::{process, thread}; - -use anyhow::{Context, anyhow, bail}; -use cookbook::WALK_DEPTH; -use cookbook::config::{CookConfig, get_config, init_config}; -use cookbook::cook::cook_build::build; -use cookbook::cook::fetch::{fetch, fetch_offline}; -use cookbook::cook::fs::create_target_dir; -use cookbook::cook::package::package; -use cookbook::cook::pty::{setup_pty, PtyOut, UnixSlavePty}; -use cookbook::recipe::CookRecipe; -use pkg::PackageName; -use pkg::package::PackageError; -use ratatui::Terminal; -use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; -use ratatui::prelude::TermionBackend; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; use termion::event::{Event, Key, MouseEvent}; use termion::input::TermRead; use termion::raw::IntoRawMode; @@ -937,7 +937,17 @@ fn run_tui_cook( log_text[start..end] .iter() - .map(|s| Line::from(s.clone())) + .map(|s| { + let text_with_colors = s + .into_text() + .unwrap_or_else(|_| Text::raw("--unrenderable line--")); + + text_with_colors + .lines + .into_iter() + .next() + .unwrap_or_else(|| Line::raw("--unrenderable line--")) + }) .collect() } else { vec![Line::from("No logs yet")] diff --git a/src/cook/pty.rs b/src/cook/pty.rs index 42876e9c..70165507 100644 --- a/src/cook/pty.rs +++ b/src/cook/pty.rs @@ -286,7 +286,7 @@ impl PtyFd { // them) and won't work in the usual way anyway. // In practice these are None, but it seems best to be move them // out in case the behavior of Command changes in the future. - child.stdin.take(); + // child.stdin.take(); child.stdout.take(); child.stderr.take(); From e30cb66990dc4b3cbddf3d669aec8a3fec38b0f7 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sun, 26 Oct 2025 22:43:02 +0700 Subject: [PATCH 21/26] Handle carriage returns --- src/bin/repo.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 6097b575..0d05bc7d 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -938,10 +938,10 @@ fn run_tui_cook( log_text[start..end] .iter() .map(|s| { - let text_with_colors = s + let line_to_render = s.rsplit('\r').next().unwrap_or(s); + let text_with_colors = line_to_render .into_text() .unwrap_or_else(|_| Text::raw("--unrenderable line--")); - text_with_colors .lines .into_iter() From 2b8915e24d68c905513d6f0b2216a000edabd8b7 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Mon, 27 Oct 2025 17:32:54 +0700 Subject: [PATCH 22/26] Fix warnings --- src/cook/fs.rs | 2 +- src/cook/pty.rs | 122 ++++++++++++------------------------------------ 2 files changed, 30 insertions(+), 94 deletions(-) diff --git a/src/cook/fs.rs b/src/cook/fs.rs index db1a0fa2..c9385260 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -1,7 +1,7 @@ use serde::Serialize; use std::{ fs, - io::{self, PipeWriter, Write}, + io::{self, Write}, path::{Path, PathBuf}, process::{self, Command, Stdio}, time::SystemTime, diff --git a/src/cook/pty.rs b/src/cook/pty.rs index 70165507..96bae8c8 100644 --- a/src/cook/pty.rs +++ b/src/cook/pty.rs @@ -1,15 +1,10 @@ use anyhow::{Error, bail}; use filedescriptor::FileDescriptor; use libc::{self, winsize}; -use portable_pty::PtySize; -use std::cell::RefCell; -use std::ffi::OsStr; -use std::io::{Read, Write}; +use std::io::Read; use std::os::fd::FromRawFd; -use std::os::unix::ffi::OsStrExt; use std::os::unix::io::AsRawFd; use std::os::unix::process::CommandExt; -use std::path::PathBuf; use std::process::Child; use std::{io, mem, ptr}; use std::{ @@ -77,6 +72,32 @@ pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result Self { + PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + } + } +} + fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { let mut master: RawFd = -1; let mut slave: RawFd = -1; @@ -104,12 +125,8 @@ fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { bail!("failed to openpty: {:?}", io::Error::last_os_error()); } - let tty_name = tty_name(slave); - let master = UnixMasterPty { fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }), - took_writer: RefCell::new(false), - tty_name, }; let slave = UnixSlavePty { fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }), @@ -170,33 +187,6 @@ impl Read for PtyFd { } } -fn tty_name(fd: RawFd) -> Option { - let mut buf = vec![0 as std::ffi::c_char; 128]; - - loop { - let res = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr(), buf.len()) }; - - if res == libc::ERANGE { - if buf.len() > 64 * 1024 { - // on macOS, if the buf is "too big", ttyname_r can - // return ERANGE, even though that is supposed to - // indicate buf is "too small". - return None; - } - buf.resize(buf.len() * 2, 0 as std::ffi::c_char); - continue; - } - - return if res == 0 { - let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }; - let osstr = OsStr::from_bytes(cstr.to_bytes()); - Some(PathBuf::from(osstr)) - } else { - None - }; - } -} - impl PtyFd { fn resize(&self, size: PtySize) -> Result<(), Error> { let ws_size = winsize { @@ -298,8 +288,6 @@ impl PtyFd { /// The file descriptor will be closed when the Pty is dropped. pub struct UnixMasterPty { fd: PtyFd, - took_writer: RefCell, - tty_name: Option, } /// Represents the slave end of a pty. @@ -334,10 +322,12 @@ impl UnixSlavePty { } impl UnixMasterPty { + #[allow(unused)] fn resize(&self, size: PtySize) -> Result<(), Error> { self.fd.resize(size) } + #[allow(unused)] fn get_size(&self) -> Result { self.fd.get_size() } @@ -346,58 +336,4 @@ impl UnixMasterPty { let fd = PtyFd(self.fd.try_clone()?); Ok(Box::new(fd)) } - - fn take_writer(&self) -> Result, Error> { - if *self.took_writer.borrow() { - anyhow::bail!("cannot take writer more than once"); - } - *self.took_writer.borrow_mut() = true; - let fd = PtyFd(self.fd.try_clone()?); - Ok(Box::new(UnixMasterWriter { fd })) - } - - fn as_raw_fd(&self) -> Option { - Some(self.fd.0.as_raw_fd()) - } - - fn tty_name(&self) -> Option { - self.tty_name.clone() - } - - fn process_group_leader(&self) -> Option { - match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } { - pid if pid > 0 => Some(pid), - _ => None, - } - } -} - -/// Represents the master end of a pty. -/// EOT will be sent, and then the file descriptor will be closed when -/// the Pty is dropped. -struct UnixMasterWriter { - fd: PtyFd, -} - -impl Drop for UnixMasterWriter { - fn drop(&mut self) { - let mut t: libc::termios = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - if unsafe { libc::tcgetattr(self.fd.0.as_raw_fd(), &mut t) } == 0 { - // EOF is only interpreted after a newline, so if it is set, - // we send a newline followed by EOF. - let eot = t.c_cc[libc::VEOF]; - if eot != 0 { - let _ = self.fd.0.write_all(&[b'\n', eot]); - } - } - } -} - -impl Write for UnixMasterWriter { - fn write(&mut self, buf: &[u8]) -> Result { - self.fd.write(buf) - } - fn flush(&mut self) -> Result<(), io::Error> { - self.fd.flush() - } } From 19f5d8ff3e8afbdc18ff89bfaff6a3bad517c04a Mon Sep 17 00:00:00 2001 From: Wildan M Date: Mon, 27 Oct 2025 21:51:35 +0700 Subject: [PATCH 23/26] Make last line visible --- src/bin/repo.rs | 94 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 0d05bc7d..f956db17 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -16,8 +16,9 @@ use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; +use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; -use std::io::{BufRead, BufReader, Read, Write, stderr, stdin, stdout}; +use std::io::{Read, Write, stderr, stdin, stdout}; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -456,7 +457,7 @@ enum StatusUpdate { StartCook(PackageName), Cooked(CookRecipe), FailCook(CookRecipe, String), - PushLog(PackageName, String), + PushLog(PackageName, Vec), FetchThreadFinished, CookThreadFinished, } @@ -485,6 +486,7 @@ struct TuiApp { active_fetch: Option, active_cook: Option, logs: HashMap>, + log_byte_buffer: HashMap>, log_scroll: usize, log_view_job: JobType, auto_scroll: bool, @@ -515,6 +517,7 @@ impl TuiApp { active_fetch: None, active_cook: None, logs: HashMap::new(), + log_byte_buffer: HashMap::new(), log_scroll: 0, auto_scroll: true, log_view_job: JobType::Fetch, @@ -538,6 +541,7 @@ impl TuiApp { StatusUpdate::StartFetch(name) => { self.active_fetch = Some(name.clone()); self.logs.insert(name.clone(), Vec::new()); + self.log_byte_buffer.insert(name.clone(), Vec::new()); self.log_scroll = 0; self.auto_scroll = true; (name.clone(), RecipeStatus::Fetching) @@ -550,16 +554,21 @@ impl TuiApp { StatusUpdate::StartCook(name) => { self.active_cook = Some(name.clone()); self.logs.insert(name.clone(), Vec::new()); + self.log_byte_buffer.insert(name.clone(), Vec::new()); (name.clone(), RecipeStatus::Cooking) } - StatusUpdate::PushLog(name, line) => { - self.logs.entry(name.clone()).or_default().push(line); - // No status change, just return the current state - if let Some((_, status)) = self.recipes.iter().find(|(r, _)| r.name == name) { - (name, status.clone()) - } else { - return; // Should not happen + StatusUpdate::PushLog(name, chunk) => { + let buffer = self.log_byte_buffer.entry(name.clone()).or_default(); + buffer.extend_from_slice(&chunk); + let log_list = self.logs.entry(name.clone()).or_default(); + while let Some(newline_pos) = buffer.iter().position(|&b| b == b'\n') { + let line_bytes = buffer.drain(..=newline_pos).collect::>(); + let line_str = String::from_utf8_lossy(&line_bytes).into_owned(); + let line_str_pos = line_str.trim_end(); + let line_str = line_str_pos.rsplit('\r').next().unwrap_or(&line_str_pos); + log_list.push(line_str.to_owned()); } + return; } StatusUpdate::Cooked(recipe) => { if self.active_cook.as_ref() == Some(&recipe.name) { @@ -570,7 +579,6 @@ impl TuiApp { } StatusUpdate::FailCook(recipe, err) => { self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); - (recipe.name.clone(), RecipeStatus::Failed(err)) } StatusUpdate::FetchThreadFinished => { @@ -892,8 +900,7 @@ fn run_tui_cook( ); f.render_stateful_widget(cook_list, cook_chunk, &mut app.cook_list_state); - let (active_name, log_text) = get_active_log(&app); - + let (active_name, log_text, log_line) = get_active_log(&app); let log_title = if let Some(active_name) = active_name { format!( " {} Log: {} ", @@ -907,8 +914,8 @@ fn run_tui_cook( let mut enable_auto_scroll = false; let mut intended_scroll_pos = 0usize; - let log_lines: Vec = if let Some(log_text) = log_text - && log_text.len() > 0 + let mut log_lines: Vec = if let Some(log_text) = log_text + && !log_text.is_empty() { let total_log_lines = log_text.len() as usize; @@ -938,8 +945,7 @@ fn run_tui_cook( log_text[start..end] .iter() .map(|s| { - let line_to_render = s.rsplit('\r').next().unwrap_or(s); - let text_with_colors = line_to_render + let text_with_colors = s .into_text() .unwrap_or_else(|_| Text::raw("--unrenderable line--")); text_with_colors @@ -953,6 +959,18 @@ fn run_tui_cook( vec![Line::from("No logs yet")] }; + if let Some(buffer) = log_line + && !buffer.is_empty() + { + let text_with_colors = handle_cr(&buffer) + .into_text() + .unwrap_or_else(|_| Text::raw("--unrenderable line--")); + + if let Some(line) = text_with_colors.lines.into_iter().next() { + log_lines.push(line); + } + } + let instruct = format!( " Keys: [c] Stop [PageUp/Down] Scroll{}{} ", match app.auto_scroll { @@ -993,11 +1011,16 @@ 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) = get_active_log(&app); + let (name, log, line) = get_active_log(&app); if let Some(name) = name && let Some(log) = log { - app.dump_logs_on_exit = Some((name.to_owned(), log.join("\n"))); + 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)); } running.store(false, Ordering::SeqCst); } @@ -1030,19 +1053,36 @@ fn run_tui_cook( Ok(app.dump_logs_on_exit) } -fn get_active_log(app: &TuiApp) -> (Option, Option<&Vec>) { +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 }; - (active_name, log_text) + 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) { @@ -1231,11 +1271,15 @@ fn spawn_log_reader( R: Read + Send + 'static, { thread::spawn(move || { - let reader = BufReader::new(&mut reader); - for line in reader.lines() { - let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); + let mut buffer = [0; 1024]; + loop { + let buf = match reader.read(&mut buffer) { + Ok(0) => break, + Ok(n) => buffer[..n].to_vec(), + Err(e) => format!("[IO Error] {}", e).into_bytes(), + }; if status_tx - .send(StatusUpdate::PushLog(package_name.clone(), line_str)) + .send(StatusUpdate::PushLog(package_name.clone(), buf)) .is_err() { // TUI thread hung up From b1fec31dff799931b2a31cd051b450df5abe0d9b Mon Sep 17 00:00:00 2001 From: Wildan M Date: Tue, 28 Oct 2025 00:20:20 +0700 Subject: [PATCH 24/26] Fix push command and category on make --- src/bin/repo.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index f956db17..c5e173b1 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -16,6 +16,7 @@ use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; +use redoxer::target; use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::io::{Read, Write, stderr, stdin, stdout}; @@ -269,6 +270,9 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec config.with_package_deps = true, @@ -336,7 +340,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result<()> { let public_path = "build/id_ed25519.pub.toml"; - let archive_path = config.repo_dir.join(recipe.name.as_str()); + let archive_path = config + .repo_dir + .join(target()) + .join(format!("{}.pkgar", recipe.name)); pkgar::extract( public_path, archive_path.as_path(), From 645a741f8d640bfb20e3ea589877ed79c865ec56 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Tue, 28 Oct 2025 00:36:44 +0700 Subject: [PATCH 25/26] Update cookbool.toml README --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9da5af7b..04e10b34 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,16 @@ Cookbook has special config to avoid repetitive args, place this file into `cook ```toml # Configuration file # This is a configuration file to avoid repetitively spelling command args. -# At the moment only mirrors here implemented but in future it will be expanded when scripts are rusted +# At the moment this configures mirror and cook configuration + +# These options has defaults set below +# These options has higher priority than env +#[cook] +#jobs = +#nonstop = false +#offline = false +#tui = true +#verbose = true [mirrors] # see list of GNU FTP mirrors at https://www.gnu.org/prep/ftp.en.html From 66413edfb30cd2e11174ef465c210bd96ef62633 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Tue, 28 Oct 2025 13:17:31 +0700 Subject: [PATCH 26/26] Don't clean build deps --- src/bin/repo.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bin/repo.rs b/src/bin/repo.rs index c5e173b1..01fd77ac 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -91,6 +91,9 @@ impl CliCommand { pub fn is_informational(&self) -> bool { *self == CliCommand::Tree || *self == CliCommand::Find } + pub fn is_building(&self) -> bool { + *self == CliCommand::Fetch || *self == CliCommand::Cook + } } impl FromStr for CliCommand { @@ -340,7 +343,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec