From d40619d66c9a5047d2c6f91f9e8ca585adbbabd1 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 04:15:10 +0700 Subject: [PATCH 1/6] Implement Display for Config --- src/config/file.rs | 35 +++++++++++++++++++++++++++++++++++ src/config/mod.rs | 30 +++++++++++++++++++++++++++--- src/config/user.rs | 27 +++++++++++++++++++++++++++ src/installer.rs | 2 +- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/config/file.rs b/src/config/file.rs index c4e88fec6b..2696b48af1 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct FileConfig { pub path: String, @@ -46,3 +48,36 @@ impl FileConfig { self } } + +impl Display for FileConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path)?; + if self.symlink { + write!(f, " -> {}", self.data)?; + } else if self.directory { + write!(f, " type=dir")?; + if self.recursive_chown { + write!(f, " chown=yes")?; + } + } else { + write!( + f, + " size={}B", + arg_parser::to_human_readable_string(self.data.len() as u64) + )?; + if self.postinstall { + write!(f, "!")?; + } + } + if let Some(uid) = self.uid { + write!(f, " uid={}", uid)?; + } + if let Some(uid) = self.uid { + write!(f, " gid={}", uid)?; + } + if let Some(mode) = self.mode { + write!(f, " mode={:3o}", mode)?; + } + Ok(()) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index a1fe76de78..d6a04259ca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,9 +1,11 @@ use std::collections::BTreeMap; +use std::fmt::Display; use std::fs; use std::mem; use std::path::{Path, PathBuf}; use anyhow::bail; +use anyhow::Context; use anyhow::Result; pub mod file; @@ -35,11 +37,11 @@ impl Config { Ok(config_data) => match toml::from_str(&config_data) { Ok(config) => config, Err(err) => { - bail!("{}: failed to decode: {}", path.display(), err); + bail!("failed to decode '{}': {}", path.display(), err); } }, Err(err) => { - bail!("{}: failed to read: {}", path.display(), err); + bail!("failed to read '{}': {}", path.display(), err); } }; @@ -47,7 +49,10 @@ impl Config { let mut configs = mem::take(&mut config.include) .into_iter() - .map(|path| Config::from_file(&config_dir.join(path))) + .map(|path| { + Config::from_file(&config_dir.join(&path)) + .with_context(|| format!("Importing from {}", path.display())) + }) .collect::>>()?; configs.push(config); // Put ourself last to ensure that it overwrites anything else. @@ -90,3 +95,22 @@ impl Config { } } } + +impl Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "files:")?; + for file in &self.files { + writeln!(f, "- {}", file)?; + } + writeln!(f, "users:")?; + for (name, user) in &self.users { + writeln!(f, "- {}:{}", name, user)?; + } + write!(f, "packages: ")?; + for name in self.packages.keys() { + write!(f, " {}", name)?; + } + writeln!(f, "")?; + Ok(()) + } +} diff --git a/src/config/user.rs b/src/config/user.rs index 9e9393de0c..862bffad0b 100644 --- a/src/config/user.rs +++ b/src/config/user.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct UserConfig { pub password: Option, @@ -14,3 +16,28 @@ pub struct GroupConfig { // FIXME move this to the UserConfig struct as extra_groups pub members: Vec, } + +impl Display for UserConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(uid) = &self.uid { + write!(f, " uid={}", uid)?; + } + if let Some(gid) = &self.gid { + write!(f, " gid={}", gid)?; + } + if let Some(name) = &self.name { + write!(f, " name={}", name)?; + } + if let Some(home) = &self.home { + write!(f, " home={}", home)?; + } + if let Some(shell) = &self.shell { + write!(f, " shell={}", shell)?; + } + if self.password.as_ref().is_some_and(|s| !s.is_empty()) { + write!(f, " password=yes")?; + } + + Ok(()) + } +} diff --git a/src/installer.rs b/src/installer.rs index 727d8862ba..1b78403f8b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -817,7 +817,7 @@ pub fn try_fast_install( } fn install_inner(config: Config, output: &Path) -> Result<()> { - println!("Install {config:#?} to {}", output.display()); + println!("Installing to {}:\n{}", output.display(), config); let cookbook = config.general.cookbook.clone(); let cookbook = cookbook.as_ref().map(|p| p.as_str()); if output.is_dir() { From fed265252d5fb8e1bfc7d8304135bb2cd0340eb6 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 04:17:11 +0700 Subject: [PATCH 2/6] Unify password prompt code --- src/bin/installer_tui.rs | 24 +++--------- src/installer.rs | 81 +++++++++++++--------------------------- 2 files changed, 32 insertions(+), 73 deletions(-) diff --git a/src/bin/installer_tui.rs b/src/bin/installer_tui.rs index e9a2774f8e..eb00f997a1 100644 --- a/src/bin/installer_tui.rs +++ b/src/bin/installer_tui.rs @@ -255,29 +255,17 @@ fn choose_disk() -> PathBuf { } } -fn choose_password() -> Option { - eprint!("installer_tui: redoxfs password (empty for none): "); - - let password = io::stdin() - .read_passwd(&mut io::stderr()) - .unwrap() - .unwrap_or(String::new()); - - eprintln!(); - - if password.is_empty() { - return None; - } - - Some(password) -} - fn main() { let root_path = Path::new("/"); let disk_path = choose_disk(); - let password_opt = choose_password(); + let Ok(password_opt) = redox_installer::prompt_password( + "redox_installer_tui: redoxfs password (empty for none)", + "redox_installer_tui: confirm password", + ) else { + process::exit(1); + }; let instant = std::time::Instant::now(); diff --git a/src/installer.rs b/src/installer.rs index 1b78403f8b..58d876b8c5 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -56,23 +56,31 @@ fn syscall_error(err: syscall::Error) -> io::Error { } /// Returns a password collected from the user (plaintext) -fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result { +pub fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result> { let stdin = io::stdin(); let mut stdin = stdin.lock(); let stdout = io::stdout(); let mut stdout = stdout.lock(); - print!("{}", prompt); - let password = stdin.read_passwd(&mut stdout)?; + for i in 0..3 { + print!("{}", prompt); + let mut password = stdin.read_passwd(&mut stdout)?; + if let Some(password) = password.as_mut() { + *password = password.trim().to_string(); + } + password.take_if(|s| s.is_empty()); - print!("\n{}", confirm_prompt); - let confirm_password = stdin.read_passwd(&mut stdout)?; + print!("\n{}", confirm_prompt); + let confirm_password = stdin.read_passwd(&mut stdout)?; - // Note: Actually comparing two Option values - if confirm_password != password { - bail!("passwords do not match"); + // Note: Actually comparing two Option values + if confirm_password == password { + return Ok(password); + } else if i < 2 { + eprintln!("passwords do not match, please try again"); + } } - Ok(password.unwrap_or("".to_string())) + bail!("passwords do not match, giving up"); } fn install_local_pkgar(cookbook: &str, target: &str, packagename: &str, dest: &Path) -> Result<()> { @@ -149,29 +157,6 @@ pub fn install_dir( output_dir: impl AsRef, cookbook: Option<&str>, ) -> Result<()> { - //let mut context = liner::Context::new(); - - macro_rules! prompt { - ($dst:expr, $def:expr, $($arg:tt)*) => { - if config.general.prompt.unwrap_or(true) { - Err(io::Error::new( - io::ErrorKind::Other, - "prompt not currently supported", - )) - // match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) { - // Ok(res) => if res.is_empty() { - // Ok($def) - // } else { - // Ok(res) - // }, - // Err(err) => Err(err) - // } - } else { - Ok($dst.unwrap_or($def)) - } - }; - } - let output_dir = output_dir.as_ref(); let output_dir = output_dir.to_owned(); @@ -222,29 +207,16 @@ pub fn install_dir( next_gid = gid + 1; } - let name = prompt!( - user.name, - username.clone(), - "{}: name (GECOS) [{}]: ", - username, - username - )?; - let home = prompt!( - user.home, - format!("/home/{}", username), - "{}: home [/home/{}]: ", - username, - username - )?; - let shell = prompt!( - user.shell, - "/bin/ion".to_string(), - "{}: shell [/bin/ion]: ", - username - )?; + let name = user.name.unwrap_or(username.clone()); + let home = user.home.unwrap_or(format!("/home/{}", username)); + let shell = user.shell.unwrap_or("/bin/ion".into()); println!("Adding user {username}:"); - println!("\tPassword: {password}"); + if password.is_empty() { + println!("\tPassword: unset"); + } else { + println!("\tPassword: set"); + } println!("\tUID: {uid}"); println!("\tGID: {gid}"); println!("\tName: {name}"); @@ -852,8 +824,7 @@ fn install_inner(config: Config, output: &Path) -> Result<()> { } /// Install RedoxFS into a new disk file, or a sysroot directory. -/// This function assumes all interactive prompts resolved by the caller, -/// so "prompt" option is ignored from this function onward. +/// This function assumes all interactive prompts resolved by the caller. pub fn install(config: Config, output: impl AsRef) -> Result<()> { install_inner(config, output.as_ref()) } From 15de2b427281a575cbe77d47a493ba0e0f7b2332 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 04:17:54 +0700 Subject: [PATCH 3/6] Add help hint, remove repo_binary --- src/bin/installer.rs | 37 +++++++++++++++++++++++++++++-------- src/bin/installer_tui.rs | 33 +++++++++++++++++++++++---------- src/config/general.rs | 9 ++------- src/installer.rs | 16 ++++++++++++++++ 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/src/bin/installer.rs b/src/bin/installer.rs index c2443a1ee6..9eb6fed6fd 100644 --- a/src/bin/installer.rs +++ b/src/bin/installer.rs @@ -10,22 +10,44 @@ use arg_parser::ArgParser; use redox_installer::{Config, PackageConfig}; +const HELP_STR: &str = r#" +redox_installer - Redox Installer. + Refer to link below for filesystem config reference: + https://doc.redox-os.org/book/configuration-settings.html + +Using redox_installer as an installer: + redox_installer [--config=file.toml] [--write-bootloader=file.img] [--live] [--no-mount] [--skip-partition] + Disk file to write + --config Path to filesystem config TOML + --write-bootloader Path to write separate EFI partition + --skip-partition Skip writing GPT partition tables + Use this only if you plan to use other partition tool + --live Use bootloader configured for live disk + --no-mount Use RedoxFS AR instead of FUSE to write files + --cookbook Use local Redox OS build system rather than downloading packages + +Using redox_installer as a configuration parser: + redox_installer --config=file.toml [--list-packages|--filesystem-size|--output-config path] + --list-packages List packages will be installed + --filesystem-size Output filesystem size in MB + --output-config Path to write the parsed config as another TOML +"#; + fn main() { let mut parser = ArgParser::new(4) .add_opt("b", "cookbook") .add_opt("c", "config") .add_opt("o", "output-config") .add_opt("", "write-bootloader") + .add_flag(&["skip-partition"]) .add_flag(&["filesystem-size"]) - .add_flag(&["r", "repo-binary"]) + .add_flag(&["r", "repo-binary"]) // TODO: Remove .add_flag(&["l", "list-packages"]) .add_flag(&["live"]) .add_flag(&["no-mount"]); parser.parse(env::args()); - // Use pre-built binaries for packages as the default. - // If not set on the command line or the filesystem config, then build packages from source. - let repo_binary = parser.found("repo-binary"); + let skip_partition = parser.found("skip-partition"); let mut config = if let Some(path) = parser.get_opt("config") { match Config::from_file(Path::new(&path)) { @@ -55,9 +77,8 @@ fn main() { ..Default::default() }); - // Add command line flags to config, command line takes priority - if repo_binary { - config.general.repo_binary = Some(true); + if skip_partition { + config.general.skip_partitions = Some(true); } if parser.found("filesystem-size") { @@ -148,7 +169,7 @@ fn main() { process::exit(1); } } else { - eprintln!("installer: output or list-packages not found"); + eprint!("{}", HELP_STR); process::exit(1); } } diff --git a/src/bin/installer_tui.rs b/src/bin/installer_tui.rs index eb00f997a1..ef5bd70d03 100644 --- a/src/bin/installer_tui.rs +++ b/src/bin/installer_tui.rs @@ -11,7 +11,11 @@ use std::{ path::{Path, PathBuf}, process, }; -use termion::input::TermRead; + +// TODO: This is not the TUI a regular user would expect it does +// 1. Linux: Implement disk listing, use "dd" to write into whole disk +// 2. Allow partitioning to allow dual boot, possibly an integration with systemd-boot/grub +// 3. Prompt everything (disk password, users, preconfigured packages, import from existing img) #[cfg(not(target_os = "redox"))] fn disk_paths(_paths: &mut Vec<(PathBuf, u64)>) {} @@ -32,7 +36,7 @@ fn disk_paths(paths: &mut Vec<(PathBuf, u64)>) { } } Err(err) => { - eprintln!("installer_tui: failed to list schemes: {}", err); + eprintln!("redox_installer_tui: failed to list schemes: {}", err); } } @@ -60,7 +64,7 @@ fn disk_paths(paths: &mut Vec<(PathBuf, u64)>) { } Err(err) => { eprintln!( - "installer_tui: failed to list '{}': {}", + "redox_installer_tui: failed to list '{}': {}", scheme.display(), err ); @@ -221,7 +225,8 @@ fn choose_disk() -> PathBuf { } if paths.is_empty() { - eprintln!("installer_tui: no drives found"); + eprintln!("redox_installer_tui: no RedoxFS partition found"); + eprintln!("redox_installer_tui: this tool is used to overwrite unmounted RedoxFS disk in Redox OS"); process::exit(1); } else { eprint!("Select a drive from 1 to {}: ", paths.len()); @@ -229,12 +234,12 @@ fn choose_disk() -> PathBuf { let mut line = String::new(); match io::stdin().read_line(&mut line) { Ok(0) => { - eprintln!("installer_tui: failed to read line: end of input"); + eprintln!("redox_installer_tui: failed to read line: end of input"); process::exit(1); } Ok(_) => (), Err(err) => { - eprintln!("installer_tui: failed to read line: {}", err); + eprintln!("redox_installer_tui: failed to read line: {}", err); process::exit(1); } } @@ -275,7 +280,11 @@ fn main() { match fs::read(&path) { Ok(ok) => ok, Err(err) => { - eprintln!("installer_tui: {}: failed to read: {}", path.display(), err); + eprintln!( + "redox_installer_tui: {}: failed to read: {}", + path.display(), + err + ); process::exit(1); } } @@ -290,7 +299,11 @@ fn main() { match fs::read(&path) { Ok(ok) => ok, Err(err) => { - eprintln!("installer_tui: {}: failed to read: {}", path.display(), err); + eprintln!( + "redox_installer_tui: {}: failed to read: {}", + path.display(), + err + ); process::exit(1); } } @@ -366,13 +379,13 @@ fn main() { match res { Ok(()) => { eprintln!( - "installer_tui: installed successfully in {:?}", + "redox_installer_tui: installed successfully in {:?}", instant.elapsed() ); process::exit(0); } Err(err) => { - eprintln!("installer_tui: failed to install: {}", err); + eprintln!("redox_installer_tui: failed to install: {}", err); process::exit(1); } } diff --git a/src/config/general.rs b/src/config/general.rs index c2a2c6c713..417ff2d966 100644 --- a/src/config/general.rs +++ b/src/config/general.rs @@ -2,18 +2,15 @@ pub struct GeneralConfig { /// Specify a path where cookbook exists, all packages will be installed locally pub cookbook: Option, - /// Allow prompts for missing information such as user password, true by default + /// Allow prompts for missing information such as user password pub prompt: Option, - /// Allow config to specify cookbook recipe or binary package as default - /// note: Not read by installer anymore, exists only for legacy reasons - pub repo_binary: Option, /// Total filesystem size in MB pub filesystem_size: Option, /// EFI partition size in MB, default to 2MB pub efi_partition_size: Option, /// Skip disk partitioning, assume whole disk is a partition pub skip_partitions: Option, - /// Set a plain text password to encrypt the disk, or empty string to prompt + /// Set a plain text password to encrypt the disk pub encrypt_disk: Option, /// Use live disk for bootloader config, default is false pub live_disk: Option, @@ -30,8 +27,6 @@ impl GeneralConfig { if let Some(cookbook) = other.cookbook { self.cookbook = Some(cookbook); } - self.prompt = other.prompt.or(self.prompt); - self.repo_binary = other.repo_binary.or(self.repo_binary); self.filesystem_size = other.filesystem_size.or(self.filesystem_size); self.efi_partition_size = other.efi_partition_size.or(self.efi_partition_size); self.skip_partitions = other.skip_partitions.or(self.skip_partitions); diff --git a/src/installer.rs b/src/installer.rs index 58d876b8c5..dde0b4696b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -33,6 +33,7 @@ pub struct DiskOption<'a> { } fn get_target() -> String { + // TODO: Configurable from filesystem config? env::var("TARGET").unwrap_or( option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()), ) @@ -191,6 +192,7 @@ pub fn install_dir( &format!("{}: enter password: ", username), &format!("{}: confirm password: ", username), )? + .unwrap_or_default() } else { String::new() }; @@ -795,6 +797,20 @@ fn install_inner(config: Config, output: &Path) -> Result<()> { if output.is_dir() { install_dir(config, output, cookbook) } else { + if !output.is_file() { + let fs_size = config.general.filesystem_size.unwrap_or(0) as u64; + // arbitrary size approximately fit just for initfs + if fs_size < 32 { + bail!("Refusing to create image disk less than 32 MB"); + } + eprintln!( + "Creating a new file to {} with size {} MB", + output.display(), + fs_size + ); + let file = fs::File::create(output)?; + file.set_len(fs_size * 1024 * 1024)?; + } let live = config.general.live_disk.unwrap_or(false); let password_opt = config.general.encrypt_disk.clone(); let password_opt = password_opt.as_ref().map(|p| p.as_bytes()); From cfc7938ecc5ea2398558854a44de4ceffa721260 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 04:53:30 +0700 Subject: [PATCH 4/6] Shorten remote packages log --- src/installer.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/installer.rs b/src/installer.rs index dde0b4696b..2e43361bb8 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -141,12 +141,20 @@ fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> any } } else { let mut library = Library::new(dest, target, Rc::new(RefCell::new(callback)))?; + let mut package_len = packages.len(); for packagename in packages { if !get_head_path(packagename, dest).exists() { - println!("Installing package from remote: {packagename}"); + if package_len == 1 { + println!("Installing package from remote: {packagename}"); + } library.install(vec![pkg::PackageName::new(packagename)?])?; + } else { + package_len -= 1; } } + if package_len != 1 { + println!("Installing {} packages from remote", package_len); + } library.apply()?; } From 753256cfafc0b4119c49faaec36ce4f90c955846 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 05:02:08 +0700 Subject: [PATCH 5/6] Strip fallocate from CI test --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15f60e63bf..4aa5e9eb7f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,5 +21,4 @@ cargo-test: - apt update && apt install -y fuse3 libfuse3-dev - cargo build --locked - cargo test - - fallocate -l 256MiB test.bin - ./target/debug/redox_installer -c res/test.toml test.bin --no-mount From c4ed26e3d4dc3adca57ac776f96225e64df7e7b3 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 31 Jan 2026 05:10:03 +0700 Subject: [PATCH 6/6] Don't confirm password if empty --- src/installer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/installer.rs b/src/installer.rs index 2e43361bb8..aaec8debf6 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -71,6 +71,10 @@ pub fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result