Files
kira-installer/src/kira_system.rs
T
2026-09-10 14:34:13 +02:00

308 lines
9.6 KiB
Rust

// <Kira Installer - universal Linux installer.>
// Copyright (C) <2026> <Kira Foundation>
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/*
Functions for creating with disk layout
*/
use crate::kira_disk_layout::*;
use std::process::Command;
use std::fs;
#[derive(Debug)]
pub struct CommandFailed{
stderr: Vec<u8>,
cmd: String,
}
impl std::fmt::Display for CommandFailed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Command failed! Command: {}; Stderr: {}", self.cmd, String::from_utf8(self.stderr.clone()).unwrap_or_else(|_|{"STDEER IS NOT VALID UTF-8".to_string()}))
}
}
impl CommandFailed {
pub fn new(cmd: &str, args: &Vec<&str>, stderr: Vec<u8>) -> Self {
Self {
stderr: stderr,
cmd: format!("{} {}", cmd, args.join(" ")),
}
}
}
#[derive(Debug)]
pub enum ExecCommandError {
IoError(std::io::Error),
FromUtf8Error(std::string::FromUtf8Error),
CommandFailed(CommandFailed),
Other(String),
}
// 2. Implement From for each wrapped error type
impl From<std::io::Error> for ExecCommandError {
fn from(err: std::io::Error) -> Self {
ExecCommandError::IoError(err)
}
}
impl From<std::string::FromUtf8Error> for ExecCommandError {
fn from(err: std::string::FromUtf8Error) -> Self {
ExecCommandError::FromUtf8Error(err)
}
}
impl From<CommandFailed> for ExecCommandError {
fn from(err: CommandFailed) -> Self {
ExecCommandError::CommandFailed(err)
}
}
impl From<String> for ExecCommandError {
fn from(err: String) -> Self {
ExecCommandError::Other(err)
}
}
impl std::fmt::Display for ExecCommandError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ExecCommandError::IoError(e) => write!(f, "IO Error: {}", e),
ExecCommandError::FromUtf8Error(e) => {
write!(f, "IO streem to String parse Error: {}", e)
}
ExecCommandError::CommandFailed(ex) => {
write!(f, "{}", ex)
}
ExecCommandError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
// Implement std::error::Error
impl std::error::Error for ExecCommandError {}
/// exec given sommand with arguments, capturing stderr in case or failure
fn exec_command(cmd: &str, args: &Vec<&str>) -> Result<(), ExecCommandError> {
let out = Command::new(cmd).args(args).output()?;
if out.status.success() {
Ok(())
} else {
Err(CommandFailed::new(cmd, args, out.stderr).into())
}
}
/// exec given sommand with arguments, returning stdout parsed to UTF-8 string
/// capturing stderr in case or failure
fn exec_command_out(cmd: &str, args: &Vec<&str>) -> Result<String, ExecCommandError> {
let out = Command::new(cmd).args(args).output()?;
if out.status.success() {
Ok(String::from_utf8(out.stdout)?)
} else {
Err(CommandFailed::new(cmd, args, out.stderr).into())
}
}
/// sync system time with chrony
/// dinitctl start chrony
pub fn dinit_sync_time_chrony() -> Result<(), ExecCommandError> {
exec_command("dinitctl", &vec!["start", "chrony"])
}
/// Install base system
/// basestrap /mnt base base-devel dinit elogind-dinit
pub fn basestrap_dinit(root_dir: &str) -> Result<(), ExecCommandError> {
exec_command("basestrap", &vec![root_dir, "base", "base-devel", "dinit", "elogind-dinit"])
}
/// Executes parted command with given arguments
fn exec_parted(args: &Vec<&str>) -> Result<String, ExecCommandError> {
exec_command_out("parted", args)
}
fn parted_get_last_part_num(dev: &BlkDev) -> Result<u32, ExecCommandError> {
// parted -m /dev/loop0 print
let res = exec_parted(&vec!["-m", dev.full_name().as_str(), "print"])?;
match res.lines().last().and_then(|last_line| {
last_line
.split_once(':')
.and_then(|(first_part, _)| u32::from_str_radix(first_part, 10).ok())
}) {
Some(n) => Ok(n),
None => Err("Error parsing parted output".to_string().into()),
}
}
/// init gpt partition table
/// parted --script $install_device mklabel gpt
/// Its blocking function
pub fn parted_init_gpt_part_table(dev: &BlkDev) -> Result<(), ExecCommandError> {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"mklabel",
"gpt",
])
.map(|_| ())
}
const LINUX_ROOT_X86_64_TYPE: &str = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709";
/// creating partition from PartInfo specification
/// It assumes that we add partitions sequentially to empty space (for part attributes settings)
/// Otherwise can produce bonkers layout
pub fn parted_mkpart(dev: &BlkDev, part: &PartInfo) -> Result<(), ExecCommandError> {
let part_end = part.start + part.size;
// gpt label should be in double qutes
let gpt_label = if let Some(s) = &part.gpt_label {
format!("\"{}\"", s)
} else {
"\"\"".to_string()
};
// creating partition
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"mkpart",
gpt_label.as_str(),
part.fs_type.to_parted_str(),
part.start.to_parted_bytes_str().as_str(),
part_end.to_parted_bytes_str().as_str(),
])?;
// set partition options depending on its role
if let Some(role) = &part.role {
// getting part number of last created partition (hopefully)
let part_id = parted_get_last_part_num(dev)?;
match role {
PartRole::EFI => {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"set",
&part_id.to_string(),
"esp",
"on",
])?;
}
// parted --script $install_device type 3 4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
PartRole::ROOT => {
exec_parted(&vec![
"--script",
dev.full_name().as_str(),
"type",
&part_id.to_string(),
LINUX_ROOT_X86_64_TYPE,
])?;
}
_ => (),
}
};
Ok(())
}
/// creating single drive bcachefs volume
pub fn bcachefs_format(
dev_name: &str,
compression: Option<String>,
password: Option<String>,
) -> Result<(), ExecCommandError> {
// sudo bcachefs format --compression=lz4 --background_compression=lz4 --encrypted /dev/loop0
// --data_checksum=none - do not make sense on single dev volumes
// sudo bcachefs format --force --compression=lz4 --background_compression=lz4 --encrypted --passphrase_file=./test.pass /dev/loop0
let mut arg = vec!["format", "--force", "--data_checksum=none"];
// if compression algorythm specified
if let Some(compresss_alg) = &compression {
arg.push(compresss_alg.as_str());
}
// if we want encrypted volume
if let Some(passw) = password {
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &passw)?;
arg.push("--encrypted");
arg.push("--passphrase_file=/tmp/bzzpsspass.txt");
}
// add dev name at the end
arg.push(dev_name);
let out = exec_command("bcachefs", &arg);
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
out
}
/// unlok encrypted bcache volume
pub fn bcachefs_unlock(dev_name: &str, pass: &str) -> Result<(), ExecCommandError> {
// bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0
let arg = vec!["unlock", "--file=/tmp/bzzpsspass.txt ", dev_name];
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &pass)?;
let out = exec_command("bcachefs", &arg);
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
out
}
/// create subvolume
pub fn bcachefs_create_subvolume(vol_path: &str) -> Result<(), ExecCommandError> {
// sudo bcachefs subvolume create /mnt/test/nyan
exec_command("bcachefs", &vec!["subvolume", "create", vol_path])
}
/// mount bcachefs
pub fn bcachefs_mount(dev_name: &str, mount_point: &str) -> Result<(), ExecCommandError> {
// sudo mount -t bcachefs /dev/loop0 /mnt/test
exec_command("mount", &vec!["-t", "bcachefs", dev_name, mount_point])
}
pub fn fatlabel(dev: &str, label: &str) -> Result<(), ExecCommandError> {
//fatlabel /dev/sda1 ESP
exec_command("fatlabel", &vec![dev, label])
}
pub fn mkfs_fat32(dev: &str, maybe_label: Option<&str>) -> Result<(), ExecCommandError> {
//mkfs.fat -F 32 /dev/sda1
exec_command("mkfs.fat", &vec!["-F", "32", dev])?;
// if need to set label
if let Some(label) = maybe_label {
fatlabel(dev, label)
} else {
Ok(())
}
}
/// mkfs.fat -F 32 /dev/sda1
/// fatlabel /dev/sda1 ESP
pub fn mkfs_esp(dev: &str) -> Result<(), ExecCommandError> {
mkfs_fat32(dev, Some("ESP"))
}
pub fn mkswap(dev: &str) -> Result<(), ExecCommandError> {
//mkswap -L SWAP /dev/sda4
exec_command("mkswap", &vec!["L", "SWAP", dev])
}