Merge branch 'cli-enh' into 'master'
Some improvement as a standalone CLI See merge request redox-os/installer!67
This commit is contained in:
@@ -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
|
||||
|
||||
+29
-8
@@ -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 <diskpath.img> [--config=file.toml] [--write-bootloader=file.img] [--live] [--no-mount] [--skip-partition]
|
||||
<diskpath.img> 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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-28
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -255,29 +260,17 @@ fn choose_disk() -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_password() -> Option<String> {
|
||||
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();
|
||||
|
||||
@@ -287,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);
|
||||
}
|
||||
}
|
||||
@@ -302,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);
|
||||
}
|
||||
}
|
||||
@@ -378,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
pub struct GeneralConfig {
|
||||
/// Specify a path where cookbook exists, all packages will be installed locally
|
||||
pub cookbook: Option<String>,
|
||||
/// Allow prompts for missing information such as user password, true by default
|
||||
/// Allow prompts for missing information such as user password
|
||||
pub prompt: Option<bool>,
|
||||
/// 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<bool>,
|
||||
/// Total filesystem size in MB
|
||||
pub filesystem_size: Option<u32>,
|
||||
/// EFI partition size in MB, default to 2MB
|
||||
pub efi_partition_size: Option<u32>,
|
||||
/// Skip disk partitioning, assume whole disk is a partition
|
||||
pub skip_partitions: Option<bool>,
|
||||
/// 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<String>,
|
||||
/// Use live disk for bootloader config, default is false
|
||||
pub live_disk: Option<bool>,
|
||||
@@ -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);
|
||||
|
||||
+27
-3
@@ -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::<Result<Vec<Config>>>()?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct UserConfig {
|
||||
pub password: Option<String>,
|
||||
@@ -14,3 +16,28 @@ pub struct GroupConfig {
|
||||
// FIXME move this to the UserConfig struct as extra_groups
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
+56
-57
@@ -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()),
|
||||
)
|
||||
@@ -56,23 +57,35 @@ 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<String> {
|
||||
pub fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<Option<String>> {
|
||||
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)?;
|
||||
if password.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Note: Actually comparing two Option<String> values
|
||||
if confirm_password != password {
|
||||
bail!("passwords do not match");
|
||||
print!("\n{}", confirm_prompt);
|
||||
let confirm_password = stdin.read_passwd(&mut stdout)?;
|
||||
|
||||
// Note: Actually comparing two Option<String> 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<()> {
|
||||
@@ -132,12 +145,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()?;
|
||||
}
|
||||
|
||||
@@ -149,29 +170,6 @@ pub fn install_dir(
|
||||
output_dir: impl AsRef<Path>,
|
||||
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();
|
||||
@@ -206,6 +204,7 @@ pub fn install_dir(
|
||||
&format!("{}: enter password: ", username),
|
||||
&format!("{}: confirm password: ", username),
|
||||
)?
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -222,29 +221,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}");
|
||||
@@ -817,12 +803,26 @@ pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
||||
}
|
||||
|
||||
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() {
|
||||
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());
|
||||
@@ -852,8 +852,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<Path>) -> Result<()> {
|
||||
install_inner(config, output.as_ref())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user