diff --git a/Cargo.lock b/Cargo.lock index 38aee5fa7e..82d2de2263 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,12 +317,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytemuck" version = "1.23.2" @@ -357,9 +351,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.1.22" -source = "git+https://github.com/tea/cc-rs?branch=riscv-abi-arch-fix#588ceacb084af41415690c57688e338a32a1f1b4" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -718,6 +714,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fnv" version = "1.0.7" @@ -1224,15 +1226,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1708,7 +1701,6 @@ version = "0.2.37" dependencies = [ "anyhow", "arg_parser", - "cc", "fatfs", "fscommon", "gpt", @@ -1719,7 +1711,6 @@ dependencies = [ "pkgar-keys", "rand", "redox-pkg", - "redox_liner", "redox_syscall", "redoxfs", "rust-argon2", @@ -1730,19 +1721,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "redox_liner" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4596948a78a2ac29268d4ee45f788789ac8c032c77cf255378e5c96ca0c779" -dependencies = [ - "bytecount", - "itertools", - "strip-ansi-escapes", - "termion", - "unicode-width 0.1.14", -] - [[package]] name = "redox_syscall" version = "0.5.17" @@ -2118,15 +2096,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "strip-ansi-escapes" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" -dependencies = [ - "vte", -] - [[package]] name = "strsim" version = "0.8.0" @@ -2503,15 +2472,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vte" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" -dependencies = [ - "memchr", -] - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index cc35de4cc5..b6ffce5e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,42 +11,60 @@ edition = "2021" [[bin]] name = "redox_installer" path = "src/bin/installer.rs" +required-features = ["installer"] [[bin]] name = "redox_installer_tui" path = "src/bin/installer_tui.rs" +required-features = ["installer"] [lib] name = "redox_installer" path = "src/lib.rs" [dependencies] -anyhow = "1.0.89" -arg_parser = "0.1.0" -cc = "1" -fatfs = "0.3.0" -fscommon = "0.1.1" -gpt = "3.0.0" -libc = "0.2.70" -pkgar = "0.1.19" -pkgar-core = "0.1.19" -pkgar-keys = "0.1.19" -rand = "0.9" -redox_liner = "0.5" -redox-pkg = { version = "0.2.8", features = ["indicatif"] } -redox_syscall = "0.5.2" -redoxfs = "0.8.0" -rust-argon2 = "0.8.2" +anyhow = { version = "1" } +arg_parser = { version = "0.1.0", optional = true } +fatfs = { version = "0.3.0", optional = true } +fscommon = { version = "0.1.1", optional = true } +gpt = { version = "3.0.0", optional = true } +libc = { version = "0.2.70" } +pkgar = { version = "0.1.19", optional = true } +pkgar-core = { version = "0.1.19", optional = true } +pkgar-keys = { version = "0.1.19", optional = true } +rand = { version = "0.9", optional = true } +redox-pkg = { version = "0.2.8", features = ["indicatif"], optional = true } +redox_syscall = { version = "0.5.2", optional = true } +redoxfs = { version = "0.8.0", optional = true } +rust-argon2 = { version = "0.8.2", optional = true } serde = "1" -serde_derive = "1.0.110" -termion = "4" -toml = "0.8" -uuid = { version = "1.4", features = ["v4"] } +serde_derive = "1.0" +termion = { version = "4", optional = true } +toml = { version = "0.8" } +uuid = { version = "1.4", features = ["v4"], optional = true } [target.'cfg(target_os = "redox")'.dependencies] libredox = "0.1" +[features] +default = ["installer"] +installer = [ + "arg_parser", + "fatfs", + "fscommon", + "gpt", + "pkgar", + "pkgar-core", + "pkgar-keys", + "rand", + "redox-pkg", + "redox_syscall", + "redoxfs", + "rust-argon2", + "termion", + "uuid", +] + [patch.crates-io] -cc-11 = { git = "https://github.com/tea/cc-rs", branch="riscv-abi-arch-fix", package = "cc" } # https://github.com/briansmith/ring/issues/1999 ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" } diff --git a/src/config/file.rs b/src/config/file.rs index f1da3c723c..c4e88fec6b 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -1,33 +1,3 @@ -use crate::Result; -use libc::{gid_t, uid_t}; - -use std::ffi::{CString, OsStr}; -use std::fs::{self, File}; -use std::io::{Error, Write}; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::{symlink, PermissionsExt}; -use std::path::Path; - -//type Result = std::result::Result; - -fn chown>(path: P, uid: uid_t, gid: gid_t, recursive: bool) -> Result<()> { - let path = path.as_ref(); - - let c_path = CString::new(path.as_os_str().as_bytes()).unwrap(); - if unsafe { libc::chown(c_path.as_ptr(), uid, gid) } != 0 { - return Err(Error::last_os_error().into()); - } - - if recursive && path.is_dir() { - for entry_res in fs::read_dir(path)? { - let entry = entry_res?; - chown(entry.path(), uid, gid, recursive)?; - } - } - - Ok(()) -} - #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct FileConfig { pub path: String, @@ -45,7 +15,6 @@ pub struct FileConfig { pub postinstall: bool, } -// TODO: Rewrite impls impl FileConfig { pub fn new_file(path: String, data: String) -> FileConfig { FileConfig { @@ -76,46 +45,4 @@ impl FileConfig { self.recursive_chown = true; self } - - pub(crate) fn create>(&self, prefix: P) -> Result<()> { - let path = self.path.trim_start_matches('/'); - let target_file = prefix.as_ref().join(path); - - if self.directory { - println!("Create directory {}", target_file.display()); - fs::create_dir_all(&target_file)?; - self.apply_perms(&target_file)?; - return Ok(()); - } else if let Some(parent) = target_file.parent() { - println!("Create file parent {}", parent.display()); - fs::create_dir_all(parent)?; - } - - if self.symlink { - println!("Create symlink {}", target_file.display()); - symlink(&OsStr::new(&self.data), &target_file)?; - Ok(()) - } else { - println!("Create file {}", target_file.display()); - let mut file = File::create(&target_file)?; - file.write_all(self.data.as_bytes())?; - - self.apply_perms(target_file) - } - } - - fn apply_perms>(&self, target: P) -> Result<()> { - let path = target.as_ref(); - let mode = self - .mode - .unwrap_or_else(|| if self.directory { 0o0755 } else { 0o0644 }); - let uid = self.uid.unwrap_or(!0); - let gid = self.gid.unwrap_or(!0); - - // chmod - fs::set_permissions(path, fs::Permissions::from_mode(mode))?; - - // chown - chown(path, uid, gid, self.recursive_chown) - } } diff --git a/src/config/file_impl.rs b/src/config/file_impl.rs new file mode 100644 index 0000000000..45928efaf0 --- /dev/null +++ b/src/config/file_impl.rs @@ -0,0 +1,72 @@ +use anyhow::Result; +use libc::{gid_t, uid_t}; + +use std::ffi::{CString, OsStr}; +use std::fs::{self, File}; +use std::io::{Error, Write}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::{symlink, PermissionsExt}; +use std::path::Path; + +fn chown>(path: P, uid: uid_t, gid: gid_t, recursive: bool) -> Result<()> { + let path = path.as_ref(); + + let c_path = CString::new(path.as_os_str().as_bytes()).unwrap(); + if unsafe { libc::chown(c_path.as_ptr(), uid, gid) } != 0 { + return Err(Error::last_os_error().into()); + } + + if recursive && path.is_dir() { + for entry_res in fs::read_dir(path)? { + let entry = entry_res?; + chown(entry.path(), uid, gid, recursive)?; + } + } + + Ok(()) +} + +// TODO: Rewrite impls +impl crate::FileConfig { + pub(crate) fn create>(&self, prefix: P) -> Result<()> { + let path = self.path.trim_start_matches('/'); + let target_file = prefix.as_ref().join(path); + + if self.directory { + println!("Create directory {}", target_file.display()); + fs::create_dir_all(&target_file)?; + self.apply_perms(&target_file)?; + return Ok(()); + } else if let Some(parent) = target_file.parent() { + println!("Create file parent {}", parent.display()); + fs::create_dir_all(parent)?; + } + + if self.symlink { + println!("Create symlink {}", target_file.display()); + symlink(&OsStr::new(&self.data), &target_file)?; + Ok(()) + } else { + println!("Create file {}", target_file.display()); + let mut file = File::create(&target_file)?; + file.write_all(self.data.as_bytes())?; + + self.apply_perms(target_file) + } + } + + fn apply_perms>(&self, target: P) -> Result<()> { + let path = target.as_ref(); + let mode = self + .mode + .unwrap_or_else(|| if self.directory { 0o0755 } else { 0o0644 }); + let uid = self.uid.unwrap_or(!0); + let gid = self.gid.unwrap_or(!0); + + // chmod + fs::set_permissions(path, fs::Permissions::from_mode(mode))?; + + // chown + chown(path, uid, gid, self.recursive_chown) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 577a7b0d0d..a1fe76de78 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -7,6 +7,8 @@ use anyhow::bail; use anyhow::Result; pub mod file; +#[cfg(feature = "installer")] +pub mod file_impl; pub mod general; pub mod package; pub mod user; diff --git a/src/installer.rs b/src/installer.rs new file mode 100644 index 0000000000..12a4d9b240 --- /dev/null +++ b/src/installer.rs @@ -0,0 +1,785 @@ +#[cfg(target_os = "redox")] +use anyhow::{anyhow, Context}; +use anyhow::{bail, Result}; +use pkg::Library; +use rand::{rngs::OsRng, TryRngCore}; +use redoxfs::{unmount_path, Disk, DiskIo, FileSystem}; +use termion::input::TermRead; + +use crate::config::file::FileConfig; +use crate::config::package::PackageConfig; +use crate::config::Config; +use crate::disk_wrapper::DiskWrapper; + +use std::{ + cell::RefCell, + collections::BTreeMap, + env, fs, + io::{self, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, + process, + rc::Rc, + sync::mpsc::channel, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +pub struct DiskOption<'a> { + pub bootloader_bios: &'a [u8], + pub bootloader_efi: &'a [u8], + pub password_opt: Option<&'a [u8]>, + pub efi_partition_size: Option, //MiB + pub skip_partitions: bool, +} + +fn get_target() -> String { + env::var("TARGET").unwrap_or( + option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()), + ) +} + +/// Converts a password to a serialized argon2rs hash, understandable +/// by redox_users. If the password is blank, the hash is blank. +fn hash_password(password: &str) -> Result { + if !password.is_empty() { + let salt = format!("{:X}", OsRng.try_next_u64()?); + let config = argon2::Config::default(); + let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?; + Ok(hash) + } else { + Ok("".into()) + } +} + +fn syscall_error(err: syscall::Error) -> io::Error { + io::Error::from_raw_os_error(err.errno) +} + +/// Returns a password collected from the user (plaintext) +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)?; + + 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"); + } + Ok(password.unwrap_or("".to_string())) +} + +fn install_local_pkgar(cookbook: &str, target: &str, packagename: &str, dest: &Path) -> Result<()> { + let head_path = get_head_path(packagename, dest); + + let public_path = format!("{cookbook}/build/id_ed25519.pub.toml",); + let pkgar_path = format!("{cookbook}/repo/{target}/{packagename}.pkgar"); + + let pkginfo_path = format!("{cookbook}/repo/{target}/{packagename}.toml"); + let pkginfo = pkg::Package::from_toml(&fs::read_to_string(pkginfo_path)?)?; + + if pkginfo.version != "" { + pkgar::extract(&public_path, &pkgar_path, dest)?; + pkgar::split(&public_path, &pkgar_path, head_path, Option::<&str>::None)?; + } + + // Recursively install any runtime dependencies. + for dep in pkginfo.depends.iter() { + let depname = dep.as_str(); + if !get_head_path(depname, dest).exists() { + println!("Installing runtime dependency for {packagename} from local repo: {depname}"); + install_local_pkgar(cookbook, target, depname, dest)?; + } + } + + Ok(()) +} + +fn get_head_path(packagename: &str, dest: &Path) -> PathBuf { + dest.join(format!("pkg/packages/{packagename}.pkgar_head")) +} + +fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> anyhow::Result<()> { + let target = &get_target(); + + let callback = pkg::callback::IndicatifCallback::new(); + + let packages: Vec<&String> = config + .packages + .iter() + .filter_map(|(packagename, package)| match package { + PackageConfig::Build(rule) if rule == "ignore" => None, + _ => Some(packagename), + }) + .collect(); + + if let Some(cookbook) = cookbook { + let dest_pkg = dest.join("pkg/packages"); + if !Path::new(&dest_pkg).is_dir() { + fs::create_dir_all(&dest_pkg)?; + } + for packagename in packages { + if !get_head_path(packagename, dest).exists() { + println!("Installing package from local repo: {}", packagename); + install_local_pkgar(cookbook, target, packagename, dest)?; + } + } + } else { + let mut library = Library::new(dest, target, Rc::new(RefCell::new(callback)))?; + for packagename in packages { + if !get_head_path(packagename, dest).exists() { + println!("Installing package from remote: {packagename}"); + library.install(vec![pkg::PackageName::new(packagename)?])?; + } + } + library.apply()?; + } + + Ok(()) +} + +pub fn install_dir( + config: Config, + 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(); + + for file in &config.files { + if !file.postinstall { + file.create(&output_dir)?; + } + } + + install_packages(&config, &output_dir, cookbook)?; + + for file in &config.files { + if file.postinstall { + file.create(&output_dir)?; + } + } + + let mut passwd = String::new(); + let mut shadow = String::new(); + let mut next_uid = 1000; + let mut next_gid = 1000; + + let mut groups = vec![]; + + for (username, user) in config.users { + // plaintext + let password = if let Some(password) = user.password { + password + } else if config.general.prompt.unwrap_or(true) { + prompt_password( + &format!("{}: enter password: ", username), + &format!("{}: confirm password: ", username), + )? + } else { + String::new() + }; + + let uid = user.uid.unwrap_or(next_uid); + + if uid >= next_uid { + next_uid = uid + 1; + } + + let gid = user.gid.unwrap_or(next_gid); + + if gid >= next_gid { + 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 + )?; + + println!("Adding user {username}:"); + println!("\tPassword: {password}"); + println!("\tUID: {uid}"); + println!("\tGID: {gid}"); + println!("\tName: {name}"); + println!("\tHome: {home}"); + println!("\tShell: {shell}"); + + FileConfig::new_directory(home.clone()) + .with_recursive_mod(0o777, uid, gid) + .create(&output_dir)?; + + if uid >= 1000 { + // Create XDG user dirs + //TODO: move to some autostart program? + for xdg_folder in &[ + "Desktop", + "Documents", + "Downloads", + "Music", + "Pictures", + "Public", + "Templates", + "Videos", + ".config", + ".local", + ".local/share", + ".local/share/Trash", + ".local/share/Trash/info", + ] { + FileConfig::new_directory(format!("{}/{}", home, xdg_folder)) + .with_mod(0o0700, uid, gid) + .create(&output_dir)?; + } + + FileConfig::new_file( + format!("{}/.config/user-dirs.dirs", home), + r#"# Produced by redox installer +XDG_DESKTOP_DIR="$HOME/Desktop" +XDG_DOCUMENTS_DIR="$HOME/Documents" +XDG_DOWNLOAD_DIR="$HOME/Downloads" +XDG_MUSIC_DIR="$HOME/Music" +XDG_PICTURES_DIR="$HOME/Pictures" +XDG_PUBLICSHARE_DIR="$HOME/Public" +XDG_TEMPLATES_DIR="$HOME/Templates" +XDG_VIDEOS_DIR="$HOME/Videos" +"# + .to_string(), + ) + .with_mod(0o0600, uid, gid) + .create(&output_dir)?; + } + + let password = hash_password(&password)?; + + passwd.push_str(&format!("{username};{uid};{gid};{name};{home};{shell}\n",)); + shadow.push_str(&format!("{username};{password}\n")); + groups.push((username.clone(), gid, vec![username])); + } + + for (group, group_config) in config.groups { + // FIXME this assumes there is no overlap between auto-created groups for users + // and explicitly specified groups. + let gid = group_config.gid.unwrap_or(next_gid); + + if gid >= next_gid { + next_gid = gid + 1; + } + + groups.push((group, gid, group_config.members)); + } + + if !passwd.is_empty() { + FileConfig::new_file("/etc/passwd".to_string(), passwd).create(&output_dir)?; + } + + if !shadow.is_empty() { + FileConfig::new_file("/etc/shadow".to_string(), shadow) + .with_mod(0o0600, 0, 0) + .create(&output_dir)?; + } + + if !groups.is_empty() { + let mut groups_data = String::new(); + + for (name, gid, members) in groups { + use std::fmt::Write; + writeln!(groups_data, "{name};x;{gid};{}", members.join(","))?; + + println!("Adding group {name}:"); + println!("\tGID: {gid}"); + println!("\tMembers: {}", members.join(", ")); + } + + FileConfig::new_file("/etc/group".to_string(), groups_data) + .with_mod(0o0600, 0, 0) + .create(&output_dir)?; + } + + Ok(()) +} + +pub fn with_redoxfs(disk: D, password_opt: Option<&[u8]>, callback: F) -> Result +where + D: Disk + Send + 'static, + F: FnOnce(FileSystem) -> Result, +{ + let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?; + let fs = FileSystem::create(disk, password_opt, ctime.as_secs(), ctime.subsec_nanos()) + .map_err(syscall_error)?; + callback(fs) +} + +pub fn with_redoxfs_mount(fs: FileSystem, callback: F) -> Result +where + D: Disk + Send + 'static, + F: FnOnce(&Path) -> Result, +{ + let mount_path = if cfg!(target_os = "redox") { + format!("file.redox_installer_{}", process::id()) + } else { + format!("/tmp/redox_installer_{}", process::id()) + }; + + if cfg!(not(target_os = "redox")) && !Path::new(&mount_path).exists() { + fs::create_dir(&mount_path)?; + } + + let (tx, rx) = channel(); + let join_handle = { + let mount_path = mount_path.clone(); + thread::spawn(move || { + let res = redoxfs::mount(fs, &mount_path, |real_path| { + tx.send(Ok(real_path.to_owned())).unwrap(); + }); + match res { + Ok(()) => (), + Err(err) => { + tx.send(Err(err)).unwrap(); + } + }; + }) + }; + + let res = match rx.recv() { + Ok(ok) => match ok { + Ok(real_path) => callback(&real_path), + Err(err) => return Err(err.into()), + }, + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "redoxfs thread did not send a result", + ) + .into()) + } + }; + + unmount_path(&mount_path)?; + + join_handle.join().unwrap(); + + if cfg!(not(target_os = "redox")) { + fs::remove_dir_all(&mount_path)?; + } + + res +} + +pub fn fetch_bootloaders( + config: &Config, + cookbook: Option<&str>, + live: bool, +) -> Result<(Vec, Vec)> { + let bootloader_dir = + PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id())); + + if bootloader_dir.exists() { + fs::remove_dir_all(&bootloader_dir)?; + } + + fs::create_dir(&bootloader_dir)?; + + let mut bootloader_config = Config::default(); + bootloader_config.general = config.general.clone(); + // Ensure a pkgar remote is available + FileConfig { + path: "/etc/pkg.d/50_redox".to_string(), + data: "https://static.redox-os.org/pkg".to_string(), + ..Default::default() + } + .create(&bootloader_dir)?; + bootloader_config + .packages + .insert("bootloader".to_string(), PackageConfig::default()); + install_packages(&bootloader_config, &bootloader_dir, cookbook)?; + + let boot_dir = bootloader_dir.join("boot"); + let bios_path = boot_dir.join(if live { + "bootloader-live.bios" + } else { + "bootloader.bios" + }); + let efi_path = boot_dir.join(if live { + "bootloader-live.efi" + } else { + "bootloader.efi" + }); + + let bios_data = if bios_path.exists() { + fs::read(bios_path)? + } else { + Vec::new() + }; + let efi_data = if efi_path.exists() { + fs::read(efi_path)? + } else { + Vec::new() + }; + + fs::remove_dir_all(&bootloader_dir)?; + + Ok((bios_data, efi_data)) +} + +//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions +pub fn with_whole_disk(disk_path: P, disk_option: &DiskOption, callback: F) -> Result +where + P: AsRef, + F: FnOnce(FileSystem>>) -> Result, +{ + let target = get_target(); + + let bootloader_efi_name = match target.as_str() { + "aarch64-unknown-redox" => "BOOTAA64.EFI", + "i586-unknown-redox" | "i686-unknown-redox" => "BOOTIA32.EFI", + "x86_64-unknown-redox" => "BOOTX64.EFI", + "riscv64gc-unknown-redox" => "BOOTRISCV64.EFI", + _ => { + bail!("target '{target}' not supported"); + } + }; + // Open disk and read metadata + eprintln!("Opening disk {}", disk_path.as_ref().display()); + let mut disk_file = DiskWrapper::open(disk_path.as_ref())?; + let disk_size = disk_file.size(); + let block_size = disk_file.block_size() as u64; + + if disk_option.skip_partitions { + return with_redoxfs( + DiskIo(fscommon::StreamSlice::new( + disk_file, + 0, + disk_size.next_multiple_of(block_size), + )?), + disk_option.password_opt, + callback, + ); + } + + let gpt_block_size = match block_size { + 512 => gpt::disk::LogicalBlockSize::Lb512, + _ => { + // TODO: support (and test) other block sizes + bail!("block size {block_size} not supported"); + } + }; + + // Calculate partition offsets + let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors + let mibi = 1024 * 1024; + + // First megabyte of the disk is reserved for BIOS partition, wich includes GPT tables + let bios_start = gpt_reserved / block_size; + let bios_end = (mibi / block_size) - 1; + + // Second megabyte of the disk is reserved for EFI partition + let efi_start = bios_end + 1; + let efi_size = if let Some(size) = disk_option.efi_partition_size { + size as u64 + } else { + 1 + }; + let efi_end = efi_start + (efi_size * mibi / block_size) - 1; + + // The rest of the disk is RedoxFS, reserving the GPT table mirror at the end of disk + let redoxfs_start = efi_end + 1; + let redoxfs_end = ((((disk_size - gpt_reserved) / mibi) * mibi) / block_size) - 1; + + // Format and install BIOS partition + { + // Write BIOS bootloader to disk + eprintln!( + "Write bootloader with size {:#x}", + disk_option.bootloader_bios.len() + ); + disk_file.seek(SeekFrom::Start(0))?; + disk_file.write_all(&disk_option.bootloader_bios)?; + + // Replace MBR tables with protective MBR + // TODO: div_ceil + let mbr_blocks = ((disk_size + block_size - 1) / block_size) - 1; + eprintln!("Writing protective MBR with disk blocks {mbr_blocks:#x}"); + gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32) + .update_conservative(&mut disk_file)?; + + // Open disk, mark it as not initialized + let mut gpt_disk = gpt::GptConfig::new() + .initialized(false) + .writable(true) + .logical_block_size(gpt_block_size) + .create_from_device(Box::new(&mut disk_file), None)?; + + // Add BIOS boot partition + let mut partitions = BTreeMap::new(); + let mut partition_id = 1; + partitions.insert( + partition_id, + gpt::partition::Partition { + part_type_guid: gpt::partition_types::BIOS, + part_guid: uuid::Uuid::new_v4(), + first_lba: bios_start, + last_lba: bios_end, + flags: 0, // TODO + name: "BIOS".to_string(), + }, + ); + partition_id += 1; + + // Add EFI boot partition + partitions.insert( + partition_id, + gpt::partition::Partition { + part_type_guid: gpt::partition_types::EFI, + part_guid: uuid::Uuid::new_v4(), + first_lba: efi_start, + last_lba: efi_end, + flags: 0, // TODO + name: "EFI".to_string(), + }, + ); + partition_id += 1; + + // Add RedoxFS partition + partitions.insert( + partition_id, + gpt::partition::Partition { + //TODO: Use REDOX_REDOXFS type (needs GPT crate changes) + part_type_guid: gpt::partition_types::LINUX_FS, + part_guid: uuid::Uuid::new_v4(), + first_lba: redoxfs_start, + last_lba: redoxfs_end, + flags: 0, + name: "REDOX".to_string(), + }, + ); + + eprintln!("Writing GPT tables: {partitions:#?}"); + + // Initialize GPT table + gpt_disk.update_partitions(partitions)?; + + // Write partition layout, returning disk file + gpt_disk.write()?; + } + + // Format and install EFI partition + { + let disk_efi_start = efi_start * block_size; + let disk_efi_end = (efi_end + 1) * block_size; + let mut disk_efi = + fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?; + + eprintln!( + "Formatting EFI partition with size {:#x}", + disk_efi_end - disk_efi_start + ); + fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?; + + eprintln!("Opening EFI partition"); + let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?; + + eprintln!("Creating EFI directory"); + let root_dir = fs.root_dir(); + root_dir.create_dir("EFI")?; + + eprintln!("Creating EFI/BOOT directory"); + let efi_dir = root_dir.open_dir("EFI")?; + efi_dir.create_dir("BOOT")?; + + eprintln!( + "Writing EFI/BOOT/{} file with size {:#x}", + bootloader_efi_name, + disk_option.bootloader_efi.len() + ); + let boot_dir = efi_dir.open_dir("BOOT")?; + let mut file = boot_dir.create_file(bootloader_efi_name)?; + file.truncate()?; + file.write_all(&disk_option.bootloader_efi)?; + } + + // Format and install RedoxFS partition + eprintln!( + "Installing to RedoxFS partition with size {:#x}", + (redoxfs_end - redoxfs_start) * block_size + ); + let disk_redoxfs = DiskIo(fscommon::StreamSlice::new( + disk_file, + redoxfs_start * block_size, + (redoxfs_end + 1) * block_size, + )?); + with_redoxfs(disk_redoxfs, disk_option.password_opt, callback) +} + +#[cfg(not(target_os = "redox"))] +pub fn try_fast_install( + _fs: &mut redoxfs::FileSystem, + _progress: F, +) -> Result { + Ok(false) +} + +/// Try fast install using live disk memory +#[cfg(target_os = "redox")] +pub fn try_fast_install( + fs: &mut redoxfs::FileSystem, + mut progress: F, +) -> Result { + use libredox::{call::MmapArgs, flag}; + use std::os::fd::AsRawFd; + use syscall::PAGE_SIZE; + + let phys = env::var("DISK_LIVE_ADDR") + .ok() + .and_then(|x| usize::from_str_radix(&x, 16).ok()) + .unwrap_or(0); + let size = env::var("DISK_LIVE_SIZE") + .ok() + .and_then(|x| usize::from_str_radix(&x, 16).ok()) + .unwrap_or(0); + if phys == 0 || size == 0 { + return Ok(false); + } + + let start = (phys / PAGE_SIZE) * PAGE_SIZE; + let end = phys + .checked_add(size) + .context("phys + size overflow")? + .next_multiple_of(PAGE_SIZE); + let size = end - start; + + let original = unsafe { + //TODO: unmap this memory + let file = fs::File::open("/scheme/memory/physical")?; + let base = libredox::call::mmap(MmapArgs { + fd: file.as_raw_fd() as usize, + addr: core::ptr::null_mut(), + offset: start as u64, + length: size, + prot: flag::PROT_READ, + flags: flag::MAP_SHARED, + }) + .map_err(|err| anyhow!("failed to mmap livedisk: {}", err))?; + + std::slice::from_raw_parts(base as *const u8, size) + }; + + struct DiskLive { + original: &'static [u8], + } + + impl redoxfs::Disk for DiskLive { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result { + let offset = (block * redoxfs::BLOCK_SIZE) as usize; + if offset + buffer.len() > self.original.len() { + return Err(syscall::Error::new(syscall::EINVAL)); + } + buffer.copy_from_slice(&self.original[offset..offset + buffer.len()]); + Ok(buffer.len()) + } + + unsafe fn write_at(&mut self, _block: u64, _buffer: &[u8]) -> syscall::Result { + Err(syscall::Error::new(syscall::EINVAL)) + } + + fn size(&mut self) -> syscall::Result { + Ok(self.original.len() as u64) + } + } + + let mut fs_old = redoxfs::FileSystem::open(DiskLive { original }, None, None, false)?; + let size_old = fs_old.header.size(); + let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE; + let used_old = size_old - free_old; + redoxfs::clone(&mut fs_old, fs, move |used| { + progress(used, used_old); + })?; + + Ok(true) +} + +fn install_inner( + config: Config, + output: &Path, + cookbook: Option<&str>, + live: bool, + write_bootloader: Option<&str>, +) -> Result<()> { + println!("Install {config:#?} to {}", output.display()); + + if output.is_dir() { + install_dir(config, output, cookbook) + } else { + let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook, live)?; + if let Some(write_bootloader) = write_bootloader { + std::fs::write(write_bootloader, &bootloader_efi)?; + } + let disk_option = DiskOption { + bootloader_bios: &bootloader_bios, + bootloader_efi: &bootloader_efi, + password_opt: None, + efi_partition_size: config.general.efi_partition_size, + skip_partitions: config.general.skip_partitions.unwrap_or(false), + }; + with_whole_disk(output, &disk_option, move |fs| { + with_redoxfs_mount(fs, move |mount_path| { + install_dir(config, mount_path, cookbook) + }) + }) + } +} +pub fn install( + config: Config, + output: impl AsRef, + cookbook: Option<&str>, + live: bool, + write_bootloader: Option<&str>, +) -> Result<()> { + install_inner(config, output.as_ref(), cookbook, live, write_bootloader) +} diff --git a/src/lib.rs b/src/lib.rs index 8ae25721a0..216478d901 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,790 +2,13 @@ extern crate serde_derive; mod config; +#[cfg(feature = "installer")] mod disk_wrapper; +#[cfg(feature = "installer")] +mod installer; +#[cfg(feature = "installer")] +pub use crate::installer::*; pub use crate::config::file::FileConfig; pub use crate::config::package::PackageConfig; pub use crate::config::Config; -use crate::disk_wrapper::DiskWrapper; - -#[cfg(target_os = "redox")] -use anyhow::{anyhow, Context}; -use anyhow::{bail, Result}; -use pkg::Library; -use rand::{rngs::OsRng, TryRngCore}; -use redoxfs::{unmount_path, Disk, DiskIo, FileSystem}; -use termion::input::TermRead; - -use std::{ - cell::RefCell, - collections::BTreeMap, - env, fs, - io::{self, Seek, SeekFrom, Write}, - path::{Path, PathBuf}, - process, - rc::Rc, - sync::mpsc::channel, - thread, - time::{SystemTime, UNIX_EPOCH}, -}; - -pub struct DiskOption<'a> { - pub bootloader_bios: &'a [u8], - pub bootloader_efi: &'a [u8], - pub password_opt: Option<&'a [u8]>, - pub efi_partition_size: Option, //MiB - pub skip_partitions: bool, -} - -fn get_target() -> String { - env::var("TARGET").unwrap_or( - option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()), - ) -} - -/// Converts a password to a serialized argon2rs hash, understandable -/// by redox_users. If the password is blank, the hash is blank. -fn hash_password(password: &str) -> Result { - if !password.is_empty() { - let salt = format!("{:X}", OsRng.try_next_u64()?); - let config = argon2::Config::default(); - let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?; - Ok(hash) - } else { - Ok("".into()) - } -} - -fn syscall_error(err: syscall::Error) -> io::Error { - io::Error::from_raw_os_error(err.errno) -} - -/// Returns a password collected from the user (plaintext) -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)?; - - 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"); - } - Ok(password.unwrap_or("".to_string())) -} - -fn install_local_pkgar(cookbook: &str, target: &str, packagename: &str, dest: &Path) -> Result<()> { - let head_path = get_head_path(packagename, dest); - - let public_path = format!("{cookbook}/build/id_ed25519.pub.toml",); - let pkgar_path = format!("{cookbook}/repo/{target}/{packagename}.pkgar"); - - let pkginfo_path = format!("{cookbook}/repo/{target}/{packagename}.toml"); - let pkginfo = pkg::Package::from_toml(&fs::read_to_string(pkginfo_path)?)?; - - if pkginfo.version != "" { - pkgar::extract(&public_path, &pkgar_path, dest)?; - pkgar::split(&public_path, &pkgar_path, head_path, Option::<&str>::None)?; - } - - // Recursively install any runtime dependencies. - for dep in pkginfo.depends.iter() { - let depname = dep.as_str(); - if !get_head_path(depname, dest).exists() { - println!("Installing runtime dependency for {packagename} from local repo: {depname}"); - install_local_pkgar(cookbook, target, depname, dest)?; - } - } - - Ok(()) -} - -fn get_head_path(packagename: &str, dest: &Path) -> PathBuf { - dest.join(format!("pkg/packages/{packagename}.pkgar_head")) -} - -fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> anyhow::Result<()> { - let target = &get_target(); - - let callback = pkg::callback::IndicatifCallback::new(); - - let packages: Vec<&String> = config - .packages - .iter() - .filter_map(|(packagename, package)| match package { - PackageConfig::Build(rule) if rule == "ignore" => None, - _ => Some(packagename), - }) - .collect(); - - if let Some(cookbook) = cookbook { - let dest_pkg = dest.join("pkg/packages"); - if !Path::new(&dest_pkg).is_dir() { - fs::create_dir_all(&dest_pkg)?; - } - for packagename in packages { - if !get_head_path(packagename, dest).exists() { - println!("Installing package from local repo: {}", packagename); - install_local_pkgar(cookbook, target, packagename, dest)?; - } - } - } else { - let mut library = Library::new(dest, target, Rc::new(RefCell::new(callback)))?; - for packagename in packages { - if !get_head_path(packagename, dest).exists() { - println!("Installing package from remote: {packagename}"); - library.install(vec![pkg::PackageName::new(packagename)?])?; - } - } - library.apply()?; - } - - Ok(()) -} - -pub fn install_dir( - config: Config, - 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(); - - for file in &config.files { - if !file.postinstall { - file.create(&output_dir)?; - } - } - - install_packages(&config, &output_dir, cookbook)?; - - for file in &config.files { - if file.postinstall { - file.create(&output_dir)?; - } - } - - let mut passwd = String::new(); - let mut shadow = String::new(); - let mut next_uid = 1000; - let mut next_gid = 1000; - - let mut groups = vec![]; - - for (username, user) in config.users { - // plaintext - let password = if let Some(password) = user.password { - password - } else if config.general.prompt.unwrap_or(true) { - prompt_password( - &format!("{}: enter password: ", username), - &format!("{}: confirm password: ", username), - )? - } else { - String::new() - }; - - let uid = user.uid.unwrap_or(next_uid); - - if uid >= next_uid { - next_uid = uid + 1; - } - - let gid = user.gid.unwrap_or(next_gid); - - if gid >= next_gid { - 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 - )?; - - println!("Adding user {username}:"); - println!("\tPassword: {password}"); - println!("\tUID: {uid}"); - println!("\tGID: {gid}"); - println!("\tName: {name}"); - println!("\tHome: {home}"); - println!("\tShell: {shell}"); - - FileConfig::new_directory(home.clone()) - .with_recursive_mod(0o777, uid, gid) - .create(&output_dir)?; - - if uid >= 1000 { - // Create XDG user dirs - //TODO: move to some autostart program? - for xdg_folder in &[ - "Desktop", - "Documents", - "Downloads", - "Music", - "Pictures", - "Public", - "Templates", - "Videos", - ".config", - ".local", - ".local/share", - ".local/share/Trash", - ".local/share/Trash/info", - ] { - FileConfig::new_directory(format!("{}/{}", home, xdg_folder)) - .with_mod(0o0700, uid, gid) - .create(&output_dir)?; - } - - FileConfig::new_file( - format!("{}/.config/user-dirs.dirs", home), - r#"# Produced by redox installer -XDG_DESKTOP_DIR="$HOME/Desktop" -XDG_DOCUMENTS_DIR="$HOME/Documents" -XDG_DOWNLOAD_DIR="$HOME/Downloads" -XDG_MUSIC_DIR="$HOME/Music" -XDG_PICTURES_DIR="$HOME/Pictures" -XDG_PUBLICSHARE_DIR="$HOME/Public" -XDG_TEMPLATES_DIR="$HOME/Templates" -XDG_VIDEOS_DIR="$HOME/Videos" -"# - .to_string(), - ) - .with_mod(0o0600, uid, gid) - .create(&output_dir)?; - } - - let password = hash_password(&password)?; - - passwd.push_str(&format!("{username};{uid};{gid};{name};{home};{shell}\n",)); - shadow.push_str(&format!("{username};{password}\n")); - groups.push((username.clone(), gid, vec![username])); - } - - for (group, group_config) in config.groups { - // FIXME this assumes there is no overlap between auto-created groups for users - // and explicitly specified groups. - let gid = group_config.gid.unwrap_or(next_gid); - - if gid >= next_gid { - next_gid = gid + 1; - } - - groups.push((group, gid, group_config.members)); - } - - if !passwd.is_empty() { - FileConfig::new_file("/etc/passwd".to_string(), passwd).create(&output_dir)?; - } - - if !shadow.is_empty() { - FileConfig::new_file("/etc/shadow".to_string(), shadow) - .with_mod(0o0600, 0, 0) - .create(&output_dir)?; - } - - if !groups.is_empty() { - let mut groups_data = String::new(); - - for (name, gid, members) in groups { - use std::fmt::Write; - writeln!(groups_data, "{name};x;{gid};{}", members.join(","))?; - - println!("Adding group {name}:"); - println!("\tGID: {gid}"); - println!("\tMembers: {}", members.join(", ")); - } - - FileConfig::new_file("/etc/group".to_string(), groups_data) - .with_mod(0o0600, 0, 0) - .create(&output_dir)?; - } - - Ok(()) -} - -pub fn with_redoxfs(disk: D, password_opt: Option<&[u8]>, callback: F) -> Result -where - D: Disk + Send + 'static, - F: FnOnce(FileSystem) -> Result, -{ - let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?; - let fs = FileSystem::create(disk, password_opt, ctime.as_secs(), ctime.subsec_nanos()) - .map_err(syscall_error)?; - callback(fs) -} - -pub fn with_redoxfs_mount(fs: FileSystem, callback: F) -> Result -where - D: Disk + Send + 'static, - F: FnOnce(&Path) -> Result, -{ - let mount_path = if cfg!(target_os = "redox") { - format!("file.redox_installer_{}", process::id()) - } else { - format!("/tmp/redox_installer_{}", process::id()) - }; - - if cfg!(not(target_os = "redox")) && !Path::new(&mount_path).exists() { - fs::create_dir(&mount_path)?; - } - - let (tx, rx) = channel(); - let join_handle = { - let mount_path = mount_path.clone(); - thread::spawn(move || { - let res = redoxfs::mount(fs, &mount_path, |real_path| { - tx.send(Ok(real_path.to_owned())).unwrap(); - }); - match res { - Ok(()) => (), - Err(err) => { - tx.send(Err(err)).unwrap(); - } - }; - }) - }; - - let res = match rx.recv() { - Ok(ok) => match ok { - Ok(real_path) => callback(&real_path), - Err(err) => return Err(err.into()), - }, - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::NotConnected, - "redoxfs thread did not send a result", - ) - .into()) - } - }; - - unmount_path(&mount_path)?; - - join_handle.join().unwrap(); - - if cfg!(not(target_os = "redox")) { - fs::remove_dir_all(&mount_path)?; - } - - res -} - -pub fn fetch_bootloaders( - config: &Config, - cookbook: Option<&str>, - live: bool, -) -> Result<(Vec, Vec)> { - let bootloader_dir = - PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id())); - - if bootloader_dir.exists() { - fs::remove_dir_all(&bootloader_dir)?; - } - - fs::create_dir(&bootloader_dir)?; - - let mut bootloader_config = Config::default(); - bootloader_config.general = config.general.clone(); - // Ensure a pkgar remote is available - FileConfig { - path: "/etc/pkg.d/50_redox".to_string(), - data: "https://static.redox-os.org/pkg".to_string(), - ..Default::default() - } - .create(&bootloader_dir)?; - bootloader_config - .packages - .insert("bootloader".to_string(), PackageConfig::default()); - install_packages(&bootloader_config, &bootloader_dir, cookbook)?; - - let boot_dir = bootloader_dir.join("boot"); - let bios_path = boot_dir.join(if live { - "bootloader-live.bios" - } else { - "bootloader.bios" - }); - let efi_path = boot_dir.join(if live { - "bootloader-live.efi" - } else { - "bootloader.efi" - }); - - let bios_data = if bios_path.exists() { - fs::read(bios_path)? - } else { - Vec::new() - }; - let efi_data = if efi_path.exists() { - fs::read(efi_path)? - } else { - Vec::new() - }; - - fs::remove_dir_all(&bootloader_dir)?; - - Ok((bios_data, efi_data)) -} - -//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions -pub fn with_whole_disk(disk_path: P, disk_option: &DiskOption, callback: F) -> Result -where - P: AsRef, - F: FnOnce(FileSystem>>) -> Result, -{ - let target = get_target(); - - let bootloader_efi_name = match target.as_str() { - "aarch64-unknown-redox" => "BOOTAA64.EFI", - "i586-unknown-redox" | "i686-unknown-redox" => "BOOTIA32.EFI", - "x86_64-unknown-redox" => "BOOTX64.EFI", - "riscv64gc-unknown-redox" => "BOOTRISCV64.EFI", - _ => { - bail!("target '{target}' not supported"); - } - }; - // Open disk and read metadata - eprintln!("Opening disk {}", disk_path.as_ref().display()); - let mut disk_file = DiskWrapper::open(disk_path.as_ref())?; - let disk_size = disk_file.size(); - let block_size = disk_file.block_size() as u64; - - if disk_option.skip_partitions { - return with_redoxfs( - DiskIo(fscommon::StreamSlice::new( - disk_file, - 0, - disk_size.next_multiple_of(block_size), - )?), - disk_option.password_opt, - callback, - ); - } - - let gpt_block_size = match block_size { - 512 => gpt::disk::LogicalBlockSize::Lb512, - _ => { - // TODO: support (and test) other block sizes - bail!("block size {block_size} not supported"); - } - }; - - // Calculate partition offsets - let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors - let mibi = 1024 * 1024; - - // First megabyte of the disk is reserved for BIOS partition, wich includes GPT tables - let bios_start = gpt_reserved / block_size; - let bios_end = (mibi / block_size) - 1; - - // Second megabyte of the disk is reserved for EFI partition - let efi_start = bios_end + 1; - let efi_size = if let Some(size) = disk_option.efi_partition_size { - size as u64 - } else { - 1 - }; - let efi_end = efi_start + (efi_size * mibi / block_size) - 1; - - // The rest of the disk is RedoxFS, reserving the GPT table mirror at the end of disk - let redoxfs_start = efi_end + 1; - let redoxfs_end = ((((disk_size - gpt_reserved) / mibi) * mibi) / block_size) - 1; - - // Format and install BIOS partition - { - // Write BIOS bootloader to disk - eprintln!( - "Write bootloader with size {:#x}", - disk_option.bootloader_bios.len() - ); - disk_file.seek(SeekFrom::Start(0))?; - disk_file.write_all(&disk_option.bootloader_bios)?; - - // Replace MBR tables with protective MBR - // TODO: div_ceil - let mbr_blocks = ((disk_size + block_size - 1) / block_size) - 1; - eprintln!("Writing protective MBR with disk blocks {mbr_blocks:#x}"); - gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32) - .update_conservative(&mut disk_file)?; - - // Open disk, mark it as not initialized - let mut gpt_disk = gpt::GptConfig::new() - .initialized(false) - .writable(true) - .logical_block_size(gpt_block_size) - .create_from_device(Box::new(&mut disk_file), None)?; - - // Add BIOS boot partition - let mut partitions = BTreeMap::new(); - let mut partition_id = 1; - partitions.insert( - partition_id, - gpt::partition::Partition { - part_type_guid: gpt::partition_types::BIOS, - part_guid: uuid::Uuid::new_v4(), - first_lba: bios_start, - last_lba: bios_end, - flags: 0, // TODO - name: "BIOS".to_string(), - }, - ); - partition_id += 1; - - // Add EFI boot partition - partitions.insert( - partition_id, - gpt::partition::Partition { - part_type_guid: gpt::partition_types::EFI, - part_guid: uuid::Uuid::new_v4(), - first_lba: efi_start, - last_lba: efi_end, - flags: 0, // TODO - name: "EFI".to_string(), - }, - ); - partition_id += 1; - - // Add RedoxFS partition - partitions.insert( - partition_id, - gpt::partition::Partition { - //TODO: Use REDOX_REDOXFS type (needs GPT crate changes) - part_type_guid: gpt::partition_types::LINUX_FS, - part_guid: uuid::Uuid::new_v4(), - first_lba: redoxfs_start, - last_lba: redoxfs_end, - flags: 0, - name: "REDOX".to_string(), - }, - ); - - eprintln!("Writing GPT tables: {partitions:#?}"); - - // Initialize GPT table - gpt_disk.update_partitions(partitions)?; - - // Write partition layout, returning disk file - gpt_disk.write()?; - } - - // Format and install EFI partition - { - let disk_efi_start = efi_start * block_size; - let disk_efi_end = (efi_end + 1) * block_size; - let mut disk_efi = - fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?; - - eprintln!( - "Formatting EFI partition with size {:#x}", - disk_efi_end - disk_efi_start - ); - fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?; - - eprintln!("Opening EFI partition"); - let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?; - - eprintln!("Creating EFI directory"); - let root_dir = fs.root_dir(); - root_dir.create_dir("EFI")?; - - eprintln!("Creating EFI/BOOT directory"); - let efi_dir = root_dir.open_dir("EFI")?; - efi_dir.create_dir("BOOT")?; - - eprintln!( - "Writing EFI/BOOT/{} file with size {:#x}", - bootloader_efi_name, - disk_option.bootloader_efi.len() - ); - let boot_dir = efi_dir.open_dir("BOOT")?; - let mut file = boot_dir.create_file(bootloader_efi_name)?; - file.truncate()?; - file.write_all(&disk_option.bootloader_efi)?; - } - - // Format and install RedoxFS partition - eprintln!( - "Installing to RedoxFS partition with size {:#x}", - (redoxfs_end - redoxfs_start) * block_size - ); - let disk_redoxfs = DiskIo(fscommon::StreamSlice::new( - disk_file, - redoxfs_start * block_size, - (redoxfs_end + 1) * block_size, - )?); - with_redoxfs(disk_redoxfs, disk_option.password_opt, callback) -} - -#[cfg(not(target_os = "redox"))] -pub fn try_fast_install( - _fs: &mut redoxfs::FileSystem, - _progress: F, -) -> Result { - Ok(false) -} - -/// Try fast install using live disk memory -#[cfg(target_os = "redox")] -pub fn try_fast_install( - fs: &mut redoxfs::FileSystem, - mut progress: F, -) -> Result { - use libredox::{call::MmapArgs, flag}; - use std::os::fd::AsRawFd; - use syscall::PAGE_SIZE; - - let phys = env::var("DISK_LIVE_ADDR") - .ok() - .and_then(|x| usize::from_str_radix(&x, 16).ok()) - .unwrap_or(0); - let size = env::var("DISK_LIVE_SIZE") - .ok() - .and_then(|x| usize::from_str_radix(&x, 16).ok()) - .unwrap_or(0); - if phys == 0 || size == 0 { - return Ok(false); - } - - let start = (phys / PAGE_SIZE) * PAGE_SIZE; - let end = phys - .checked_add(size) - .context("phys + size overflow")? - .next_multiple_of(PAGE_SIZE); - let size = end - start; - - let original = unsafe { - //TODO: unmap this memory - let file = fs::File::open("/scheme/memory/physical")?; - let base = libredox::call::mmap(MmapArgs { - fd: file.as_raw_fd() as usize, - addr: core::ptr::null_mut(), - offset: start as u64, - length: size, - prot: flag::PROT_READ, - flags: flag::MAP_SHARED, - }) - .map_err(|err| anyhow!("failed to mmap livedisk: {}", err))?; - - std::slice::from_raw_parts(base as *const u8, size) - }; - - struct DiskLive { - original: &'static [u8], - } - - impl redoxfs::Disk for DiskLive { - unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result { - let offset = (block * redoxfs::BLOCK_SIZE) as usize; - if offset + buffer.len() > self.original.len() { - return Err(syscall::Error::new(syscall::EINVAL)); - } - buffer.copy_from_slice(&self.original[offset..offset + buffer.len()]); - Ok(buffer.len()) - } - - unsafe fn write_at(&mut self, _block: u64, _buffer: &[u8]) -> syscall::Result { - Err(syscall::Error::new(syscall::EINVAL)) - } - - fn size(&mut self) -> syscall::Result { - Ok(self.original.len() as u64) - } - } - - let mut fs_old = redoxfs::FileSystem::open(DiskLive { original }, None, None, false)?; - let size_old = fs_old.header.size(); - let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE; - let used_old = size_old - free_old; - redoxfs::clone(&mut fs_old, fs, move |used| { - progress(used, used_old); - })?; - - Ok(true) -} - -fn install_inner( - config: Config, - output: &Path, - cookbook: Option<&str>, - live: bool, - write_bootloader: Option<&str>, -) -> Result<()> { - println!("Install {config:#?} to {}", output.display()); - - if output.is_dir() { - install_dir(config, output, cookbook) - } else { - let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook, live)?; - if let Some(write_bootloader) = write_bootloader { - std::fs::write(write_bootloader, &bootloader_efi)?; - } - let disk_option = DiskOption { - bootloader_bios: &bootloader_bios, - bootloader_efi: &bootloader_efi, - password_opt: None, - efi_partition_size: config.general.efi_partition_size, - skip_partitions: config.general.skip_partitions.unwrap_or(false), - }; - with_whole_disk(output, &disk_option, move |fs| { - with_redoxfs_mount(fs, move |mount_path| { - install_dir(config, mount_path, cookbook) - }) - }) - } -} -pub fn install( - config: Config, - output: impl AsRef, - cookbook: Option<&str>, - live: bool, - write_bootloader: Option<&str>, -) -> Result<()> { - install_inner(config, output.as_ref(), cookbook, live, write_bootloader) -}