diff --git a/Cargo.lock b/Cargo.lock index 749958945..cfa705a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,9 +1494,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libredox" -version = "0.1.4" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.1", "libc", @@ -1536,6 +1536,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + [[package]] name = "memchr" version = "2.7.5" @@ -2078,9 +2084,9 @@ dependencies = [ [[package]] name = "redox-scheme" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c00025a04f76fdcf72c15f10c7a12d9f2fdde93e539be9a57d5d632c4158a9e" +checksum = "4da6a0251965958189cdfd5ebb66f99754db4aa165394300aa2b958525d94b64" dependencies = [ "libredox", "redox_syscall", @@ -2104,6 +2110,7 @@ dependencies = [ "portable-pty", "ratatui", "redox-pkg", + "redox_installer", "redoxer", "regex", "serde", @@ -2115,9 +2122,9 @@ dependencies = [ [[package]] name = "redox_installer" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0895f18dfc8825af0b8c52d687b9eca499c9a53a6c37a248b8ffc5369b2e481f" +checksum = "86b0e2e9b588faacd83ff68ce0f4dcd7a47e7477ff3405c63c79712bcd550931" dependencies = [ "anyhow", "arg_parser", @@ -2126,6 +2133,7 @@ dependencies = [ "fscommon", "gpt", "libc", + "libredox", "pkgar 0.1.18", "pkgar-core 0.1.18", "pkgar-keys 0.1.18", @@ -2157,9 +2165,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.1", ] @@ -2194,9 +2202,9 @@ dependencies = [ [[package]] name = "redoxer" -version = "0.2.54" +version = "0.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ead20eb76f54e16ecc3e678daca948d49497588a777148ac60d99a67dab8a2b0" +checksum = "ee006e9945ef5ad5b9d877465b5bbf94f37d37aa2d0765c8bcb7a6cd401775b6" dependencies = [ "dirs 6.0.0", "proc-mounts", @@ -2209,13 +2217,14 @@ dependencies = [ [[package]] name = "redoxfs" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03016c4a1366227740e6ee755e492c7b45656bbc43728980c7764c4ddcde73d" +checksum = "063eedabd74ddf71810e72aae1c73f3485ffc7b1e757d9466b9099046c05d7be" dependencies = [ "aes", "argon2", "base64ct", + "bitflags 2.9.1", "endian-num", "env_logger", "fuser", @@ -2223,6 +2232,7 @@ dependencies = [ "libc", "libredox", "log", + "lz4_flex", "range-tree", "redox-path", "redox-scheme", diff --git a/Cargo.toml b/Cargo.toml index e21950d79..75415bb1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ toml = "0.8" walkdir = "2.3.1" filedescriptor = "0.8.3" ansi-to-tui = "7.0.0" +redox_installer = "0.2.37" [dependencies.ratatui] version = "0.29.0" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 01fd77ace..97c4066c2 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -7,7 +7,8 @@ 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 cookbook::cook::tree::{display_tree_entry, format_size}; +use cookbook::recipe::{BuildKind, CookRecipe}; use pkg::PackageName; use pkg::package::PackageError; use ratatui::Terminal; @@ -16,9 +17,10 @@ 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 redox_installer::PackageConfig; use redoxer::target; use std::borrow::Cow; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::io::{Read, Write, stderr, stdin, stdout}; use std::path::PathBuf; use std::process::Command; @@ -56,6 +58,7 @@ const REPO_HELP_STR: &str = r#" --with-package-deps include package deps --all apply to all recipes in --category= apply to all recipes in / + --filesystem= override recipes config using installer file cook env and their defaults: CI= set to any value to disable TUI @@ -71,6 +74,7 @@ struct CliConfig { repo_dir: PathBuf, sysroot_dir: PathBuf, category: Option, + filesystem: Option, with_package_deps: bool, all: bool, cook: CookConfig, @@ -92,7 +96,10 @@ impl CliCommand { *self == CliCommand::Tree || *self == CliCommand::Find } pub fn is_building(&self) -> bool { - *self == CliCommand::Fetch || *self == CliCommand::Cook + *self == CliCommand::Fetch || *self == CliCommand::Cook || *self == CliCommand::Tree + } + pub fn is_cleaning(&self) -> bool { + *self == CliCommand::Clean || *self == CliCommand::Unfetch } } @@ -143,6 +150,7 @@ impl CliConfig { with_package_deps: false, cook: get_config().cook.clone(), all: false, + filesystem: None, }) } } @@ -188,6 +196,9 @@ fn main_inner() -> anyhow::Result<()> { } return Ok(()); } + if command == CliCommand::Tree { + return handle_tree(&recipe_names, &config); + } let verbose = config.cook.verbose; for recipe in &recipe_names { @@ -251,7 +262,7 @@ fn repo_inner( 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::Tree => unreachable!(), CliCommand::Find => println!("{}", recipe.dir.display()), }) } @@ -268,6 +279,12 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec config.repo_dir = PathBuf::from(value), "--sysroot" => config.sysroot_dir = PathBuf::from(value), "--category" => config.category = Some(PathBuf::from(value)), + "--filesystem" => { + config.filesystem = Some({ + let r = redox_installer::Config::from_file(&PathBuf::from(value)); + r.context("Unable to read filesystem installer config")? + }) + } _ => { eprintln!("Error: Unknown flag with value: {}", arg); process::exit(1); @@ -309,14 +326,14 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec, PackageError>>()? } else { if recipe_names.is_empty() { - bail!("Error: No recipe names provided and --all flag was not used."); + if let Some(conf) = config.filesystem.as_ref() { + recipe_names = conf + .packages + .iter() + .filter_map(|(f, v)| { + // same logic as list_installer + match v { + PackageConfig::Build(rule) if rule == "source" || rule == "local" => {} + PackageConfig::Build(rule) if rule == "binary" || rule == "ignore" => { + return None; + } + _ if conf.general.repo_binary == Some(true) => { + return None; + } + _ => {} + } + PackageName::new(f).ok() + }) + .collect(); + } else { + bail!( + "Error: No recipe names or filesystem config provided and --all flag was not used." + ); + } } if config.with_package_deps { recipe_names = CookRecipe::get_package_deps_recursive(&recipe_names, WALK_DEPTH) @@ -344,7 +384,11 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec {} + // keep local changes + PackageConfig::Build(rule) if rule == "local" => recipe.recipe.source = None, + // should not gone here, but if it does, then some deps need it + PackageConfig::Build(rule) if rule == "binary" || rule == "ignore" => { + recipe.recipe.source = None; + recipe.recipe.build = cookbook::recipe::BuildRecipe { + kind: BuildKind::Remote, + dependencies: Vec::new(), + }; + } + PackageConfig::Build(rule) => { + return Err(anyhow!( + // Fail fast because we could risk losing local changes if "local" was typo'ed + "Invalid pkg config {} = \"{}\"\nExpecting either 'source', 'local', 'binary' or 'ignore'", + recipe.name.as_str(), + rule + )); + } + _ => { + if conf.general.repo_binary == Some(true) { + // same reason as Build("binary") + recipe.recipe.source = None; + recipe.recipe.build = cookbook::recipe::BuildRecipe { + kind: BuildKind::Remote, + dependencies: Vec::new(), + }; + } + } + } + } + } + } if command.is_informational() { // avoid extra data that clobber stdout @@ -445,6 +529,34 @@ fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { )) } +fn handle_tree(recipes: &Vec, _config: &CliConfig) -> anyhow::Result<()> { + let recipe_map: HashMap<&PackageName, &CookRecipe> = + recipes.iter().map(|r| (&r.name, r)).collect(); + + let mut total_size: u64 = 0; + let mut visited: HashSet = HashSet::new(); + + let roots: Vec<&CookRecipe> = recipes.iter().filter(|r| !r.is_deps).collect(); + + let num_roots = roots.len(); + + for (i, root) in roots.iter().enumerate() { + display_tree_entry( + &root.name, + &recipe_map, + "", + i == num_roots - 1, + &mut visited, + &mut total_size, + )?; + } + + println!(""); + println!("Estimated image size: {}", format_size(total_size)); + + Ok(()) +} + // // ------------- TUI SPECIFIC CODE ------------------- // diff --git a/src/cook.rs b/src/cook.rs index 14c752f88..d0946e0a2 100644 --- a/src/cook.rs +++ b/src/cook.rs @@ -5,3 +5,4 @@ pub mod fs; pub mod package; pub mod pty; pub mod script; +pub mod tree; diff --git a/src/cook/tree.rs b/src/cook/tree.rs new file mode 100644 index 000000000..be709d470 --- /dev/null +++ b/src/cook/tree.rs @@ -0,0 +1,100 @@ +use std::{ + collections::{HashMap, HashSet}, + fs::read_to_string, +}; + +use anyhow::{Context, anyhow}; +use pkg::{Package, PackageName}; + +use crate::{cook::fs::create_target_dir, recipe::CookRecipe}; + +pub fn display_tree_entry( + package_name: &PackageName, + recipe_map: &HashMap<&PackageName, &CookRecipe>, + prefix: &str, + is_last: bool, + visited: &mut HashSet, + total_size: &mut u64, +) -> anyhow::Result<()> { + let line_prefix = if is_last { "└── " } else { "├── " }; + let child_prefix = if is_last { " " } else { "│ " }; + + let cook_recipe = match recipe_map.get(package_name) { + Some(r) => r, + None => { + // TODO: This is a dependency, but it's not in recipe list + println!( + "{}{}{} (dependency info missing)", + prefix, line_prefix, package_name + ); + return Ok(()); + } + }; + + let package_dir = &cook_recipe.dir; + let pkg_path = create_target_dir(package_dir) + .map_err(|e| anyhow!(e))? + .join("stage.pkgar"); + let pkg_toml = create_target_dir(package_dir) + .map_err(|e| anyhow!(e))? + .join("stage.toml"); + + let deduped = visited.contains(package_name); + let (size_str, pkg_size) = match (std::fs::metadata(&pkg_path), deduped) { + (_, true) => ("".to_string(), 0), + (Ok(meta), _) => { + let size = meta.len(); + (format!("[{}]", format_size(size)), size) + } + (Err(_), _) => ("(not built)".to_string(), 0), + }; + + println!("{}{}{} {}", prefix, line_prefix, package_name, size_str); + + if deduped { + return Ok(()); + } + + visited.insert(package_name.clone()); + *total_size += pkg_size; + let pkg_meta: Package; + + let mut all_deps_set: HashSet<&PackageName> = HashSet::new(); + if let Ok(pkg_toml_str) = read_to_string(&pkg_toml) { + // more accurate with auto deps + pkg_meta = toml::from_str(&pkg_toml_str) + .context(format!("Unable to parse {}", pkg_toml.display()))?; + all_deps_set.extend(pkg_meta.depends.iter()); + } else { + all_deps_set.extend(cook_recipe.recipe.package.dependencies.iter()); + } + + if all_deps_set.is_empty() { + return Ok(()); + } + + let sorted_deps: Vec<&PackageName> = all_deps_set.into_iter().collect(); + let deps_count = sorted_deps.len(); + for (i, dep_name) in sorted_deps.iter().enumerate() { + display_tree_entry( + dep_name, + recipe_map, + &format!("{}{}", prefix, child_prefix), + i == deps_count - 1, + visited, + total_size, + )?; + } + + Ok(()) +} + +pub fn format_size(bytes: u64) -> String { + if bytes == 0 { + return "0 B".to_string(); + } + const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; + let i = (bytes as f64).log(1024.0).floor() as usize; + let size = bytes as f64 / 1024.0_f64.powi(i as i32); + format!("{:.2} {}", size, UNITS[i]) +} diff --git a/src/recipe.rs b/src/recipe.rs index 98ca43607..6dd6da09a 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -180,7 +180,7 @@ pub struct CookRecipe { pub name: PackageName, pub dir: PathBuf, pub recipe: Recipe, - /// If true, the source will not be checked for freshness + /// If false, it's listed on install config pub is_deps: bool, }