Red Bear OS installer baseline from 0.1.0 pre-patched archive

This commit is contained in:
Red Bear OS
2026-06-27 09:21:43 +03:00
commit 451813b2da
27 changed files with 13421 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
use std::fmt::Display;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct FileConfig {
pub path: String,
pub data: String,
#[serde(default)]
pub symlink: bool,
#[serde(default)]
pub directory: bool,
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
#[serde(default)]
pub recursive_chown: bool,
#[serde(default)]
pub postinstall: bool,
}
impl FileConfig {
pub fn new_file(path: String, data: String) -> FileConfig {
FileConfig {
path,
data,
..Default::default()
}
}
pub fn new_directory(path: String) -> FileConfig {
FileConfig {
path,
data: String::new(),
directory: true,
..Default::default()
}
}
pub fn with_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
self.mode = Some(mode);
self.uid = Some(uid);
self.gid = Some(gid);
self
}
pub fn with_recursive_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
self.with_mod(mode, uid, gid);
self.recursive_chown = true;
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(())
}
}
+88
View File
@@ -0,0 +1,88 @@
use anyhow::{Context, 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<P: AsRef<Path>>(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<P: AsRef<Path>>(&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)
.with_context(|| format!("failed to create directory {}", target_file.display()))?;
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)
.with_context(|| format!("failed to create file parent {}", parent.display()))?;
}
if self.symlink {
println!("Create symlink {} to {}", target_file.display(), self.data);
if target_file.is_symlink() {
fs::remove_file(&target_file).with_context(|| {
format!("failed to remove old symlink {}", target_file.display())
})?;
}
symlink(&OsStr::new(&self.data), &target_file).with_context(|| {
format!(
"failed to create symlink {} to {}",
target_file.display(),
self.data
)
})?;
Ok(())
} else {
println!("Create file {}", target_file.display());
let mut file = File::create(&target_file)
.with_context(|| format!("failed to create file {}", target_file.display()))?;
file.write_all(self.data.as_bytes())?;
self.apply_perms(target_file)
}
}
fn apply_perms<P: AsRef<Path>>(&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))
.with_context(|| format!("failed to set permissions on {}", path.display()))?;
// chown
chown(path, uid, gid, self.recursive_chown)
.with_context(|| format!("failed to chown {}", path.display()))
}
}
+42
View File
@@ -0,0 +1,42 @@
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
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
pub prompt: 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
pub encrypt_disk: Option<String>,
/// Use live disk for bootloader config, default is false
pub live_disk: Option<bool>,
/// If set, write bootloader disk into this path
pub write_bootloader: Option<String>,
/// Use AR to write files instead of FUSE-based mount
/// (bypasses FUSE, but slower and requires namespaced context such as "podman unshare")
pub no_mount: Option<bool>,
}
impl GeneralConfig {
/// Merge two config, "other" is more dominant
pub(super) fn merge(&mut self, other: GeneralConfig) {
if let Some(cookbook) = other.cookbook {
self.cookbook = Some(cookbook);
}
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);
if let Some(encrypt_disk) = other.encrypt_disk {
self.encrypt_disk = Some(encrypt_disk);
}
self.live_disk = other.live_disk.or(self.live_disk);
if let Some(write_bootloader) = other.write_bootloader {
self.write_bootloader = Some(write_bootloader);
}
self.no_mount = other.no_mount.or(self.no_mount);
}
}
+134
View File
@@ -0,0 +1,134 @@
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;
use crate::PackageConfig;
pub mod file;
#[cfg(feature = "installer")]
pub mod file_impl;
pub mod general;
pub mod package;
pub mod user;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub include: Vec<PathBuf>,
#[serde(default)]
pub general: general::GeneralConfig,
#[serde(default)]
pub packages: BTreeMap<String, package::PackageConfig>,
#[serde(default)]
pub files: Vec<file::FileConfig>,
#[serde(default)]
pub users: BTreeMap<String, user::UserConfig>,
#[serde(default)]
pub groups: BTreeMap<String, user::GroupConfig>,
}
impl Config {
/// Load installer config from a TOML path
pub fn from_file(path: &Path) -> Result<Self> {
let mut config: Config = match fs::read_to_string(&path) {
Ok(config_data) => match toml::from_str(&config_data) {
Ok(config) => config,
Err(err) => {
bail!("failed to decode '{}': {}", path.display(), err);
}
},
Err(err) => {
bail!("failed to read '{}': {}", path.display(), err);
}
};
let config_dir = path.parent().unwrap();
let mut configs = mem::take(&mut config.include)
.into_iter()
.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.
config = configs.remove(0);
for other_config in configs {
config.merge(other_config);
}
Ok(config)
}
/// Load hardcoded install config to fetch bootloaders
pub fn bootloader_config() -> Self {
let mut bootloader_config = Config::default();
// TODO: This is unused
bootloader_config.files.push(file::FileConfig {
path: "/etc/pkg.d/50_redox".to_string(),
data: "https://static.redox-os.org/pkg".to_string(),
..Default::default()
});
bootloader_config
.packages
.insert("bootloader".to_string(), PackageConfig::default());
bootloader_config
}
pub fn merge(&mut self, other: Config) {
assert!(self.include.is_empty());
assert!(other.include.is_empty());
let Config {
include: _,
general: other_general,
packages: other_packages,
files: other_files,
users: other_users,
groups: other_groups,
} = other;
self.general.merge(other_general);
for (package, package_config) in other_packages {
self.packages.insert(package, package_config);
}
self.files.extend(other_files);
for (user, user_config) in other_users {
self.users.insert(user, user_config);
}
for (group, group_config) in other_groups {
self.groups.insert(group, group_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(())
}
}
+19
View File
@@ -0,0 +1,19 @@
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PackageConfig {
Empty,
Build(String),
// TODO: Sum type
Spec {
version: Option<String>,
git: Option<String>,
path: Option<String>,
},
}
impl Default for PackageConfig {
fn default() -> Self {
Self::Empty
}
}
+43
View File
@@ -0,0 +1,43 @@
use std::fmt::Display;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct UserConfig {
pub password: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub name: Option<String>,
pub home: Option<String>,
pub shell: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct GroupConfig {
pub gid: Option<u32>,
// 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(())
}
}