Merge branch 'installer-read' into 'master'

Read from installer config

See merge request redox-os/cookbook!668
This commit is contained in:
Jeremy Soller 2025-10-31 09:25:28 -06:00
commit 49a246c2b9
6 changed files with 245 additions and 21 deletions

34
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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 <cookbook_dir>
--category=<category> apply to all recipes in <cookbook_dir>/<category>
--filesystem=<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<PathBuf>,
filesystem: Option<redox_installer::Config>,
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<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
"--repo" => 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<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
let command = command.ok_or(anyhow!("Error: No command specified."))?;
let command: CliCommand = str::parse(&command)?;
let recipes = if config.all || config.category.is_some() {
let mut recipes = if config.all || config.category.is_some() {
if !recipe_names.is_empty() {
bail!("Do not specify recipe names when using the --all or --category flag.");
}
if config.all && config.category.is_some() {
bail!("Do not specify both --all and --category flag.");
}
if config.all && command != CliCommand::Clean && command != CliCommand::Unfetch {
if config.all && !command.is_cleaning() {
// because read_recipe is false by logic below
// some recipes on wip folders are invalid anyway
bail!(
@ -336,7 +353,30 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
.collect::<Result<Vec<CookRecipe>, 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<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
}
if command.is_building() {
CookRecipe::get_build_deps_recursive(&recipe_names, !config.with_package_deps)?
CookRecipe::get_build_deps_recursive(
&recipe_names,
// In CliCommand::Cook, is_deps==true will make it skip checking source
command == CliCommand::Tree || !config.with_package_deps,
)?
} else {
recipe_names
.iter()
@ -352,6 +396,46 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
.collect()
}
};
if let Some(conf) = config.filesystem.as_ref()
&& !command.is_cleaning()
{
for recipe in recipes.iter_mut() {
if let Some(recipe_conf) = conf.packages.get(recipe.name.as_str()) {
match recipe_conf {
// build from source as usual
PackageConfig::Build(rule) if rule == "source" => {}
// 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<CookRecipe>, _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<PackageName> = 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 -------------------
//

View File

@ -5,3 +5,4 @@ pub mod fs;
pub mod package;
pub mod pty;
pub mod script;
pub mod tree;

100
src/cook/tree.rs Normal file
View File

@ -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<PackageName>,
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])
}

View File

@ -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,
}